更新可能なD3.jsチャートに向けて
公開: 2022-03-11序章
D3.jsは、MikeBostockによって開発されたデータ視覚化用のオープンソースライブラリです。 D3はデータ駆動型ドキュメントの略で、その名前が示すように、ライブラリを使用すると、開発者はデータに基づいてDOM要素を簡単に生成および操作できます。 ライブラリの機能によって制限されることはありませんが、D3.jsは通常SVG要素で使用され、ベクターデータの視覚化を最初から開発するための強力なツールを提供します。
簡単な例から始めましょう。 5kレースのトレーニングをしていて、先週の毎日の走行マイル数の横棒グラフを作成するとします。
var milesRun = [2, 5, 4, 1, 2, 6, 5]; d3.select('body').append('svg') .attr('height', 300) .attr('width', 800) .selectAll('rect') .data(milesRun) .enter() .append('rect') .attr('y', function (d, i) { return i * 40 }) .attr('height', 35) .attr('x', 0) .attr('width', function (d) { return d*100}) .style('fill', 'steelblue');
実際の動作を確認するには、bl.ocks.orgで確認してください。
このコードがおなじみのようであれば、それは素晴らしいことです。 そうでない場合は、ScottMurrayのチュートリアルがD3.jsの使用を開始するための優れたリソースであることがわかりました。
D3.jsで何百時間も開発してきたフリーランサーとして、私の開発パターンは進化を遂げ、常に最も包括的なクライアントとユーザーエクスペリエンスを作成することを最終目標としています。 後で詳しく説明するように、再利用可能なチャートのMike Bostockのパターンは、任意の数の選択で同じチャートを実装するための実証済みの真の方法を提供しました。 ただし、チャートが初期化されると、その制限が実現されます。 この方法でD3の遷移と更新パターンを使用したい場合、データへの変更は、チャートが生成されたのと同じスコープ内で完全に処理される必要がありました。 実際には、これは、フィルター、ドロップダウン選択、スライダー、およびサイズ変更オプションをすべて同じ関数スコープ内に実装することを意味しました。
これらの制限を直接経験した後、私はD3.jsの全機能を活用する方法を作成したいと思いました。 たとえば、完全に独立したコンポーネントのドロップダウンで変更をリッスンし、古いデータから新しいデータへのグラフの更新をシームレスにトリガーします。 完全な機能を備えたチャートコントロールを引き渡すことができるようにしたかったのですが、それは論理的でモジュール式の方法で行いました。 結果は更新可能なチャートパターンであり、このパターンを作成するための完全な進行をウォークスルーします。
D3.jsチャートパターン進行
ステップ1:構成変数
D3.jsを使用して視覚化を開発し始めたとき、構成変数を使用してグラフの仕様をすばやく定義および変更することが非常に便利になりました。 これにより、グラフでさまざまな長さと値のデータをすべて処理できるようになりました。 走行距離を表示したのと同じコードで、一時的な中断なしに、より長い温度リストを表示できるようになりました。
var highTemperatures = [77, 71, 82, 87, 84, 78, 80, 84, 86, 72, 71, 68]; var height = 300; var width = 800; var barPadding = 1; var barSpacing = height / highTemperatures.length; var barHeight = barSpacing - barPadding; var maxValue = d3.max(highTemperatures); var widthScale = width / maxValue; d3.select('body').append('svg') .attr('height', height) .attr('width', width) .selectAll('rect') .data(highTemperatures) .enter() .append('rect') .attr('y', function (d, i) { return i * barSpacing }) .attr('height', barHeight) .attr('x', 0) .attr('width', function (d) { return d*widthScale}) .style('fill', 'steelblue');
実際の動作を確認するには、bl.ocks.orgで確認してください。
データのサイズと値の両方に基づいて、バーの高さと幅がどのようにスケーリングされるかに注目してください。 1つの変数が変更され、残りが処理されます。
ステップ2:機能による簡単な繰り返し
ビジネスロジックの一部を抽象化することで、データの一般化されたテンプレートを処理する準備ができている、より用途の広いコードを作成できます。 次のステップは、このコードを生成関数にラップすることです。これにより、初期化が1行に短縮されます。 この関数は、データ、DOMターゲット、およびデフォルトの構成変数を上書きするために使用できるオプションオブジェクトの3つの引数を取ります。 これを行う方法を見てみましょう。
var milesRun = [2, 5, 4, 1, 2, 6, 5]; var highTemperatures = [77, 71, 82, 87, 84, 78, 80, 84, 86, 72, 71, 68, 75, 73, 80, 85, 86, 80]; function drawChart(dom, data, options) { var width = options.width || 800; var height = options.height || 200; var barPadding = options.barPadding || 1; var fillColor = options.fillColor || 'steelblue'; var barSpacing = height / data.length; var barHeight = barSpacing - barPadding; var maxValue = d3.max(data); var widthScale = width / maxValue; d3.select(dom).append('svg') .attr('height', height) .attr('width', width) .selectAll('rect') .data(data) .enter() .append('rect') .attr('y', function (d, i) { return i * barSpacing }) .attr('height', barHeight) .attr('x', 0) .attr('width', function (d) { return d*widthScale}) .style('fill', fillColor); } var weatherOptions = {fillColor: 'coral'}; drawChart('#weatherHistory', highTemperatures, weatherOptions); var runningOptions = {barPadding: 2}; drawChart('#runningHistory', milesRun, runningOptions);
実際の動作を確認するには、bl.ocks.orgで確認してください。
このコンテキストでのD3.jsの選択についてメモすることも重要です。 d3.selectAll('rect')
ような一般的な選択は常に避ける必要があります。 SVGがページの他の場所に存在する場合、ページ上のすべてのrect
が選択の一部になります。 代わりに、渡されたDOM参照を使用して、要素を追加および更新するときに参照できる1つのsvg
オブジェクトを作成します。 バーのような参照を使用すると、D3.jsを再度選択する必要がなくなるため、この手法はグラフ生成の実行時間を改善することもできます。
ステップ3:メソッドの連鎖と選択
構成オブジェクトを使用する以前のスケルトンはJavaScriptライブラリ全体で非常に一般的ですが、D3.jsの作成者であるMike Bostockは、再利用可能なグラフを作成するための別のパターンを推奨しています。 つまり、Mike Bostockは、ゲッターセッターメソッドを使用したクロージャとしてチャートを実装することを推奨しています。 チャートの実装にいくらかの複雑さを加える一方で、メソッドチェーンを使用するだけで、呼び出し元にとって構成オプションの設定が非常に簡単になります。
// Using Mike Bostock's Towards Reusable Charts Pattern function barChart() { // All options that should be accessible to caller var width = 900; var height = 200; var barPadding = 1; var fillColor = 'steelblue'; function chart(selection){ selection.each(function (data) { var barSpacing = height / data.length; var barHeight = barSpacing - barPadding; var maxValue = d3.max(data); var widthScale = width / maxValue; d3.select(this).append('svg') .attr('height', height) .attr('width', width) .selectAll('rect') .data(data) .enter() .append('rect') .attr('y', function (d, i) { return i * barSpacing }) .attr('height', barHeight) .attr('x', 0) .attr('width', function (d) { return d*widthScale}) .style('fill', fillColor); }); } chart.width = function(value) { if (!arguments.length) return margin; width = value; return chart; }; chart.height = function(value) { if (!arguments.length) return height; height = value; return chart; }; chart.barPadding = function(value) { if (!arguments.length) return barPadding; barPadding = value; return chart; }; chart.fillColor = function(value) { if (!arguments.length) return fillColor; fillColor = value; return chart; }; return chart; } var milesRun = [2, 5, 4, 1, 2, 6, 5]; var highTemperatures = [77, 71, 82, 87, 84, 78, 80, 84, 86, 72, 71, 68, 75, 73, 80, 85, 86, 80]; var runningChart = barChart().barPadding(2); d3.select('#runningHistory') .datum(milesRun) .call(runningChart); var weatherChart = barChart().fillColor('coral'); d3.select('#weatherHistory') .datum(highTemperatures) .call(weatherChart);
実際の動作を確認するには、bl.ocks.orgで確認してください。

チャートの初期化では、D3.js選択を使用して、関連データをバインドし、 this
コンテキストとしてDOM選択をジェネレーター関数に渡します。 ジェネレーター関数はデフォルト変数をクロージャーでラップし、呼び出し元がチャートオブジェクトを返す構成関数を使用したメソッドチェーンを通じてこれらを変更できるようにします。 これにより、呼び出し元は、同じグラフを一度に複数の選択範囲にレンダリングしたり、1つのグラフを使用して、かさばるオプションオブジェクトを渡さずに、同じグラフを異なるデータの異なる選択範囲にレンダリングしたりできます。
ステップ4:更新可能なチャートの新しいパターン
Mike Bostockによって提案された以前のパターンは、チャート開発者として、ジェネレーター関数内で多くのパワーを提供します。 1セットのデータと渡されたチェーン構成が与えられると、そこからすべてを制御します。 データを内部から変更する必要がある場合は、最初から再描画するのではなく、適切なトランジションを使用できます。 ウィンドウのサイズ変更などもエレガントに処理でき、省略形のテキストの使用や軸ラベルの変更などのレスポンシブ機能を作成できます。
しかし、データがジェネレーター関数の範囲外から変更された場合はどうなりますか? または、チャートのサイズをプログラムで変更する必要がある場合はどうなりますか? 新しいデータと新しいサイズ構成を使用して、チャート関数を再度呼び出すことができます。 すべてが再描画され、出来上がり。 問題が解決しました。
残念ながら、このソリューションには多くの問題があります。
まず、ほとんど必然的に不要な初期化計算を行っています。 幅をスケーリングするだけで複雑なデータ操作を行うのはなぜですか? これらの計算は、チャートが最初に初期化されるときに必要になる場合がありますが、更新する必要があるすべての更新で必要になるわけではありません。 プログラムによるリクエストごとにいくつかの変更が必要です。開発者として、これらの変更が何であるかを正確に把握しています。 これ以上でもそれ以下でもありません。 さらに、チャートスコープ内では、必要な多くのもの(SVGオブジェクト、現在のデータ状態など)にすでにアクセスして、変更を簡単に実装できます。
上記の棒グラフの例を例にとってみましょう。 幅を更新したい場合、チャート全体を再描画することで更新すると、最大データ値の検索、バーの高さの計算、これらすべてのSVG要素のレンダリングなど、多くの不要な計算がトリガーされます。 実際、 width
が新しい値に割り当てられると、必要な変更は次のとおりです。
width = newWidth; widthScale = width / maxValue; bars.attr('width', function(d) { return d*widthScale}); svg.attr('width', width);
しかし、それはさらに良くなります。 チャートの履歴がいくらかあるので、D3に組み込まれているトランジションを使用して、チャートを更新し、簡単にアニメーション化できます。 上記の例を続けると、 width
にトランジションを追加するのは変更するのと同じくらい簡単です
bars.attr('width', function(d) { return d*widthScale});
に
bars.transition().duration(1000).attr('width', function(d) { return d*widthScale});
さらに良いことに、ユーザーが新しいデータセットを渡すことを許可すると、D3の更新選択(Enter、Update、Exit)を使用して、新しいデータに遷移を適用することもできます。 しかし、どのようにして新しいデータを許可するのでしょうか。 思い出してください。以前の実装では、次のような新しいグラフが作成されました。
d3.select('#weatherHistory') .datum(highTemperatures) .call(weatherChart);
データをD3.jsの選択にバインドし、再利用可能なグラフと呼びました。 データへの変更は、新しいデータを同じ選択にバインドすることによって行う必要があります。 理論的には、古いパターンを使用して既存のデータの選択を精査し、結果を新しいデータで更新することができます。 これは実装が面倒で複雑であるだけでなく、既存のチャートが同じタイプと形式であるという仮定が必要になります。
代わりに、JavaScriptジェネレーター関数の構造にいくつかの変更を加えることで、呼び出し元がメソッドチェーンを通じて外部から簡単に変更を促すことができるチャートを作成できます。 構成とデータが設定されてそのまま放置される前は、チャートが初期化された後でも、呼び出し元は次のようなことを実行できます。
weatherChart.width(420);
その結果、既存のチャートから新しい幅へのスムーズな移行が実現します。 不必要な計算がなく、滑らかな遷移で、結果は幸せなクライアントです。
この追加機能には、開発者の労力がわずかに増加します。 しかし、歴史的に時間の価値があることがわかった努力。 更新可能なグラフのスケルトンは次のとおりです。
function barChart() { // All options that should be accessible to caller var data = []; var width = 800; //... the rest var updateData; var updateWidth; //... the rest function chart(selection){ selection.each(function () { // //draw the chart here using data, width // updateWidth = function() { // use width to make any changes }; updateData = function() { // use D3 update pattern with data } }); } chart.data = function(value) { if (!arguments.length) return data; data = value; if (typeof updateData === 'function') updateData(); return chart; }; chart.width = function(value) { if (!arguments.length) return width; width = value; if (typeof updateWidth === 'function') updateWidth(); return chart; }; //... the rest return chart; }
完全に実装されていることを確認するには、bl.ocks.orgで確認してください。
新しい構造を確認しましょう。 以前のクロージャー実装からの最大の変更は、更新機能の追加です。 前に説明したように、これらの関数はD3.jsトランジションと更新パターンを活用して、新しいデータまたはチャート構成に基づいて必要な変更をスムーズに行います。 呼び出し元がこれらにアクセスできるようにするために、関数がプロパティとしてチャートに追加されます。 さらに簡単にするために、初期構成と更新の両方が同じ機能で処理されます。
chart.width = function(value) { if (!arguments.length) return width; width = value; if (typeof updateWidth === 'function') updateWidth(); return chart; };
チャートが初期化されるまで、 updateWidth
は定義されないことに注意してください。 undefined
の場合、構成変数はグローバルに設定され、チャートクロージャーで使用されます。 チャート関数が呼び出された場合、すべての遷移はupdateWidth
関数に渡されます。この関数は、変更されたwidth
変数を使用して必要な変更を行います。 このようなもの:
updateWidth = function() { widthScale = width / maxValue; bars.transition().duration(1000).attr('width', function(d) { return d*widthScale}); svg.transition().duration(1000).attr('width', width); };
この新しい構造では、チャートのデータは、D3.jsの選択にバインドするのではなく、他の構成変数と同じようにメソッドチェーンを介して渡されます。 違い:
var weatherChart = barChart(); d3.select('#weatherHistory') .datum(highTemperatures) .call(weatherChart);
これは次のようになります。
var weatherChart = barChart().data(highTemperatures); d3.select('#weatherHistory') .call(weatherChart);
そこで、いくつかの変更を加え、開発者の努力を少し加えました。利点を見てみましょう。
新しい機能のリクエストがあるとします。「ユーザーが高温と低温を切り替えることができるように、ドロップダウンを追加します。 そして、あなたがそれにいる間、色も変えてください。」 現在のチャートをクリアし、新しいデータをバインドし、最初から再描画する代わりに、低温が選択されたときに簡単な呼び出しを行うことができるようになりました。
weatherChart.data(lowTemperatures).fillColor('blue');
魔法を楽しんでください。 計算を保存するだけでなく、視覚化が更新されるときに、以前は不可能だった新しいレベルの理解を視覚化に追加します。
ここでは、遷移に関する重要な注意事項が必要です。 同じ要素で複数の遷移をスケジュールする場合は注意してください。 新しいトランジションを開始すると、以前に実行されていたトランジションが暗黙的にキャンセルされます。 もちろん、1つのD3.jsで開始されたトランジションの要素で複数の属性またはスタイルを変更できますが、複数のトランジションが同時にトリガーされる場合があります。 このような場合、更新関数を作成するときに、親要素と子要素で同時遷移を使用することを検討してください。
哲学の変化
Mike Bostockは、チャート生成をカプセル化する方法としてクロージャを導入しています。 彼のパターンは、多くの場所で異なるデータを使用して同じチャートを作成するために最適化されています。 ただし、D3.jsを使用していた数年間で、優先順位にわずかな違いがありました。 チャートの1つのインスタンスを使用して、異なるデータで同じ視覚化を作成する代わりに、私が導入した新しいパターンにより、呼び出し元はチャートの複数のインスタンスを簡単に作成できます。各インスタンスは、初期化後でも完全に変更できます。 さらに、これらの各更新はチャートの現在の状態へのフルアクセスで処理されるため、開発者は不要な計算を排除し、D3.jsの機能を利用して、よりシームレスなユーザーエクスペリエンスとクライアントエクスペリエンスを作成できます。