JavaScriptおよびHTML5 Canvasでのサンバーストチャートの実装



みなさんこんにちは! 今日は、わずか数百行のコードでjs + canvasで独自のグラフィックを作成する方法についてお話したいと思います。 しかし同時に、幾何学の学校のコースを思い出してください。



なぜ



ブラウザでグラフを作成できる十分なライブラリがあります。 そして、 d3jsは実際に事実上の標準になっています。 ただし、サンバースト図が本当に必要だったため、数十または数百キロバイトのライブラリコードを一緒にドラッグしたくありませんでした。 また、このような図がモバイルブラウザで迅速に機能する必要もありました。したがって、svgの実装は、キャンバスと比較して後者のパフォーマンスが低いため、適切ではありませんでした。 チャートのスケーリングは提供されませんでした。 また、そのようなことを書くことは、 abcパスをたどる素晴らしい方法です。



視覚化するもの



視覚化のためのデータは、おおよそ次の形式です。



サンプルデータ
{ name: 'day', value: 24 * 60 * 60, children: [ { name: 'work', value: 9 * 60 * 60, children: [ { name: 'coding', value: 6 * 60 * 60, children: [ {name: 'python', value: 4 * 60 * 60}, {name: 'js', value: 2 * 60 * 60} ] }, {name: 'communicate', value: 1.5 * 60 * 60} ] }, {name: 'sleep', value: 7 * 60 * 60}, ... };
      
      







つまり 階層構造を持ち、各ノードには名前と値(値)があります。 次のように表示する必要があります。









この場合、さまざまな領域をクリックしてダイアグラムをスケーリングする機能を提供する必要があります。 実際にデータツリー内を移動します。 データ編集と同様に、すなわち ノードの追加と削除。



特徴





その結果、私はこれこれのいくつかのハイブリッドを取得したいと思います。



実装



データの場所



キャンバス上でデータを均等に配布するには、最初にそれらの最大のネストを見つける必要があります。 これは、次の再帰関数を使用して簡単に実行できます。



 function maxDeep(data) { var deeps = []; for (var i = 0, l = (data.children || []).length; i < l; i++) { deeps.push(maxDeep(data.children[i])); } return 1 + Math.max.apply(Math, deeps.length ? deeps : [0]); };
      
      





データのネストの深さにより、ダイアグラム内のレイヤー(リング)の数が決まります。 合計ですべてのリングがmin(canvas.width、canvas.height)を占める必要があります。 黄金比の規則に従って、リングの幅を中央(最大サイズ)からキャンバスの端(最小サイズ)に縮小します。 後続の各リングは、前のリングの1.62倍の薄さです。 したがって、再帰式があります。



 x + x / 1.62 + x / (1.62^2) + ... + x / (1,62^(n-1)) = min(canvas.width, canvas.height)
      
      





ここで、nはレイヤーの数、xはルートノードの厚さです。 したがって、xは次の関数を使用して簡単に見つけることができます。



 function rootNodeWidth(n, canvasWidth, canvasHeight) { var canvasSize = Math.min(canvasWidth, canvasHeight), div = 1; for (var i = 1; i < n; i++) { div += 1 / Math.pow(1.62, i); } return canvasSize / 2 / div; //  2 ,      . };
      
      





さらに、チャートを描画するときに、現在のレイヤーの厚さを1.62で割って、次のレイヤーの厚さを取得します。



チャートノードの視覚化



ノードのレンダリングに直接進む前に、いくつかの計算を実行する必要があります。 特に、各ノードの高さを計算し、ノードの長さ、始点の角度を決定し、色を割り当てる必要があります。



高さの決定については、すでに上記で説明しました。 角度は次のように計算されます: 中央ノードは円です。 横方向のコンポーネント間の角度は360度(2 Piラジアン)で、1つの線にマージされます(視覚化されていません)。 中央ノードの厚さは、この円の半径です。



後続のすべてのノードは、中央ノードを包む弧です。





最初の(中央ではない)レベルのノード。



アークの長さ(つまり、そのサイドコンポーネント間の角度)は、このアークに対応するデータの値(値)と、このアークの親ノードのデータ値の比率に基づいて計算されます。 したがって、中央ノードの値が= 100で、それに埋め込まれた第1レベルのノードの値が= 50である場合、後者の角度は180度(50/100 = Pi / 2 Pi)になります。 このルールは、親に関連して各ノードに再帰的に適用されます。 ノードに2つ以上の相続人がいる場合、最初の相続人の最大角度は2番目の相続人の最小角度になります。 すべての計算は時計回りに進みます。





ノードと値のアークの長さの比率。



ノードの色は、使用可能なセットから順番に割り当てられます。 上記の計算は、次の関数によって実行できます。



 function calcMetaData(dataRootNode, rootNodeWidth) { var startWidth = rootNodeWidth, meta = { root: { data: dataRootNode, color: pickColor(), angles: {begin: 0, end: 2 * Math.PI, abs: 2 * Math.PI}, //   -  width: startWidth, offset: 0, children: [], scale: 1 } }, sibling; function calcChildMetaData(childDatum, parentMeta, sibling, scale) { var meta = { data: childDatum, color: pickColor(), parent: parentMeta, width: parentMeta.width / scale, offset: parentMeta.offset + parentMeta.width, children: [], scale: parentMeta.scale / scale }, childSibling; meta.angles = {abs: parentMeta.angles.abs * childDatum.value / parentMeta.data.value}; meta.angles.begin = sibling ? sibling.angles.end : parentMeta.angles.begin; meta.angles.end = meta.angles.begin + meta.angles.abs; for (var i = 0, l = (childDatum.children || []).length; i < l; i++) { childSibling = calcChildMetaData(childDatum.children[i], meta, childSibling, scale); meta.children.push(childSibling); } return meta; } for (var i = 0, l = (dataRootNode.children || []).length; i < l; i++) { if (dataRootNode.children[i].value > dataRootNode.value) { console.error('Child value greater than parent value.', dataRootNode.children[i], dataRootNode); continue; } sibling = calcChildMetaData(dataRootNode.children[i], meta.root, sibling, 1.62); meta.root.children.push(sibling); } return meta; };
      
      





中央ノードを描画する最も簡単な方法。 これを行うには、 arc()関数を使用して閉じた円弧を作成し、色で塗りつぶします。



 var nodeMeta = {width: 20px, color: 'green', angles: {begin: 0, end: 2 * Math.PI}}; //    var origin = {x: 250, y: 250}; //   var ctx = canvas.getContext('2d'); function drawRootNodeBody(nodeMeta, origin, ctx) { ctx.beginPath(); ctx.arc(origin.x, origin.y, nodeMeta.width, nodeMeta.angles.begin, nodeMeta.angles.end); //  ctx.fillStyle = nodeMeta.color; //   -    ctx.fill(); ctx.strokeStyle = 'white'; //    -   ctx.stroke(); }
      
      





残りのノードは、もう少し興味深いものです。 実際、目的の形状の閉じたパスを描画し、色で塗りつぶす必要があります。





ノードを形成するキャンバスへのパス。



描画セクションのシーケンスは矢印で示されています。 どちらの側で描画を開始するかは重要ではありません。 外側のアークから始めます。 中心を外れたノードのレンダリング機能:



 function drawChildNodeBody(nodeMeta, origin, ctx) { ctx.beginPath(); ctx.arc(origin.x, origin.y, nodeMeta.offset, nodeMeta.angles.begin, nodeMeta.angles.end); //   //    ctx.save(); ctx.translate(origin.x, origin.y); //         ctx.rotate(nodeMeta.angles.end); //  ,     //          ctx.lineTo(nodeMeta.offset + nodeMeta.width, 0); ctx.restore(); //        //   ctx.arc(origin.x, origin.y, nodeMeta.offset + nodeMeta.width, nodeMeta.angles.end, nodeMeta.angles.begin, true); //      -    . ctx.closePath(); ctx.fillStyle = nodeMeta.hover ? 'red' : nodeMeta.color; ctx.fill(); ctx.strokeStyle = 'white'; ctx.stroke(); }
      
      





コンテキストを回して側面の弧の1つを描画する代わりに、ノードの側面コンポーネントの垂直(または水平)と回転角度の間の角度であるMath.sin()(またはMath.cos())を使用できます。 確かに、キャンバスの回転のおかげで、コードは大幅に簡素化されています。 この点がレンダリングのパフォーマンスにどのように影響するのでしょうか。



指定された座標によるチャートノードの定義



クリック(またはタッチ)およびホバーノードでダイアグラムのスケーリングをさらに実装するには、キャンバス上の座標によってダイアグラムノードを決定する方法を学習する必要があります。 これは、デカルト座標系から極座標系への移行を使用して簡単に行えます。



まず、グラフの中心(レンダリングの開始前に計算)からクリックポイント(座標はonclickイベントオブジェクトから既知)までの距離と、X軸とグラフの中心とこのポイントを結ぶセグメントとの間の角度を決定します。







これを行うには、次の関数が必要です。



 function cartesianCoordsToPolarCoords(point, origin) { var difX = point.x - origin.x, difY = point.y - origin.y, distance = Math.sqrt(difX * difX + difY * difY), angle = Math.acos(difX / distance); if (difY < 0) { angle = 2 * Math.PI - angle; } return {dist: distance, angle: angle}; };
      
      





以前、各ノードに関するメタデータの計算を行い、レベルの幅、開始角度、ノードの各アークの長さなどを計算したことを思い出すと、極座標でポインターの下にあるノードを非常に簡単に見つけることができます:



 function getNodeByPolarCoords(point, origin, metaData) { function _findNode(point, nodeMeta) { //      if (nodeMeta.offset >= point.dist) { //       ,  distance, //   ,        . return null; } if (nodeMeta.offset < point.dist && point.dist <= nodeMeta.offset + nodeMeta.width) { //  ,       . if (nodeMeta.angles.begin < point.angle && point.angle <= nodeMeta.angles.end) { return nodeMeta; } } else { // We need to go deeper.     . var node; for (var i = 0, l = (nodeMeta.children || []).length; i < l; i++) { if (node = _findNode(point, nodeMeta.children[i])) { return node; } } } return null; } return _findNode(point, metaData.root); };
      
      







グラフの詳細の変更



ダイアグラムの端にあるノードをさらに詳しく調べるには、ノードをクリックしてスケーリングを実装する必要があります。 実際、このシナリオはダイアグラムを再描画するのと同等ですが、ルートデータノードとして、クリックされたノードを選択する必要があります。 同様に、中央ノードをクリックすると、データ構造内でその祖先を見つけるだけで十分であり、存在する場合はダイアグラムを描画し、後者をルートノードとして選択します。 コードを100%再利用します。



おわりに



js + canvasでのグラフィックスの実装は、実際にはそれほど難しい作業ではありませんでした。 座標系の紙に少し暖かい鉛筆を描き、学校のコースからの罪と罪の定義を思い出すだけで十分です。



実例はgithub.ioにあります。

コードはgithub リポジトリで入手できます



All Articles