GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

グラフ 種類 9選 — 計算で描くデータ可視化チャート

グラフ 種類を実際の計算式だけで描き分けたセットです。九種類のうち三つはリボンのように流れ、三つは棒で途切れ、二つは円にまとまり、一つは漏斗状に狭まる。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

グラフ 種類 9つ — 値ひとつが描画をどう変えるか

グラフ 種類 名前を人気順に並べる記事は多いですが、この記事ではひとつの値が描画のどこをどれだけ変えるかという基準で並べています。01番は流量ひとつをリボンの太さに変え、02〜04番は累計値・段階の割合・カテゴリの大きさをそれぞれ棒の高さ・台形の幅・円の半径に変え、05番はソートした配列を箱ひげ図の形に変えます。続く06〜08番は複数の値を軸・ブラシ・順位で同時に比較し、09番は始値・高値・安値・終値という4つの数値を1本のローソク足にまとめて締めくくります。この順番で読むと、同じ考え方がJavaScriptの数値ひとつから図形ひとつへとスケールしていく様子がわかります。

チャート種類別コード

このセットの裏にはindex.html/style.scssの組が9つあり、チャート種類ごとにキャンバスが1枚ずつ対応していて、以下のコードはデモが実際に実行しているファイルからそのまま抜き出しています。サンキーダイアグラムとは何かをスクリーンショットではなくリボンの計算式で知りたいなら、まず01番から読むのがおすすめです。

01太さが流量を表すサンキー・リボン図

リボン一本の太さをその区間が運ぶ値の大きさに合わせて計算し、太いリボンほど多くの量が流れていることを一目で示します。予算配分・流入経路レポートのように、数字を読まなくても流量の大小を比較したい場面に向いています。

太さが流量を表すサンキー・リボン図
新しいタブで開く

01 · 太さが流量を表すサンキー・リボン図 — 計測 累積面積 31.643% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SANKEY_DRAW_DURATION_MSRIBBON_STAGGER_MSbuildLayout
  var rand = seedRand(425011);
  var LEFT = ['A', 'B', 'C'];
  var RIGHT = ['P', 'Q'];
  var FLOW = [
    [18 + rand() * 40, 0],
    [10 + rand() * 30, 14 + rand() * 36],
    [0, 22 + rand() * 44]
  ];

  // リボンが左から右へ描かれるのにかかる時間(ms)。変形ではこれを900から1800に変える。
  var SANKEY_DRAW_DURATION_MS = 900;
  var RIBBON_STAGGER_MS = 90;

  function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
  function lerp(a, b, t) { return a + (b - a) * t; }
  function easeOutCubic(x) { return 1 - Math.pow(1 - x, 3); }

  function buildLayout(cw, ch) {

01番は19/23 フレームで塗り直しが起き、累積面積31.6%・平均強度88.9を記録しました。

02増減をつなぐウォーターフォール・ブリッジチャート

ある棒が終わる高さから次の棒が始まるよう累積値を計算し、増減の過程が橋のようにつながって見えます。月次売上増減のブリーフィング資料のように、どの変化が合計を一番動かしたかを見せたい場面に向いています。

増減をつなぐウォーターフォール・ブリッジチャート
新しいタブで開く

02 · 増減をつなぐウォーターフォール・ブリッジチャート — 計測 累積面積 10.019% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BAR_GROW_MSBAR_STAGGER_MSCOLOR_DOWN
  var BAR_GROW_MS = 260;
  var BAR_STAGGER_MS = 210;

  function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
  function lerp(a, b, t) { return a + (b - a) * t; }
  function easeOutCubic(x) { return 1 - Math.pow(1 - x, 3); }

  // bars[0]とbars[5]は合計、bars[1..4]は増減。anchorはバーの伸びるアニメーションの起点。
  var bars = [
    { kind: 'total', value: startVal, anchor: 0, target: startVal },
    { kind: 'delta', value: deltas[0], anchor: running[0], target: running[1] },
    { kind: 'delta', value: deltas[1], anchor: running[1], target: running[2] },
    { kind: 'delta', value: deltas[2], anchor: running[2], target: running[3] },
    { kind: 'delta', value: deltas[3], anchor: running[3], target: running[4] },
    { kind: 'total', value: endVal, anchor: 0, target: endVal }
  ];

  var COLOR_TOTAL = '#0a4fe8';
  var COLOR_UP = '#2b9d74';
  var COLOR_DOWN = '#c2361a';

02番は累積面積10.0%・平均強度171.7と、9つの中でピクセル単位の変化が最も鋭く、22/23 フレームで動きました。

03段階ごとに幅が狭まるファネルチャート

各段階の値の割合ぶんだけ台形の幅を計算して狭め、前段階からの離脱が幅の差として現れます。登録から決済までの転換ファネルレポート向けで、最も離脱が大きい段階が見た目で分かります。

段階ごとに幅が狭まるファネルチャート
新しいタブで開く

03 · 段階ごとに幅が狭まるファネルチャート — 計測 累積面積 29.776% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

makeFunnelDATASET_ASTAGE_COLORS
  var rand = seedRand(425031);
  function makeFunnel() {
    var v = 100;
    var out = [v];
    for (var i = 0; i < 4; i += 1) {
      v = v * (0.55 + rand() * 0.25);
      out.push(v);
    }
    return out;
  }
  var DATASET_A = makeFunnel();
  var DATASET_B = makeFunnel();

  var STAGE_COLORS = ['#2f6df6', '#c2361a', '#2b9d74', '#8a5cd6', '#0a4fe8'];

03番は累積面積29.8%・平均強度40.7、21/23 フレームで動きました。

04値の大きさぶん円が押し合うバブルパックチャート

値が大きい項目ほど半径を大きく取り、円同士が重ならなくなるまで位置を繰り返し計算し、大きさだけで値を比較できます。カテゴリ別売上比率の比較カードのように、軸を置く余白がない小さなダッシュボードのタイルに向いています。

値の大きさぶん円が押し合うバブルパックチャート
新しいタブで開く

04 · 値の大きさぶん円が押し合うバブルパックチャート — 計測 累積面積 25.791% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BUBBLE_PACK_COLLISION_ITERATIONSsnapshotsradii
  var COUNT = 7;
  var K = 3.1;
  var BUBBLE_PACK_COLLISION_ITERATIONS = 30;

  function buildPack(seed) {
    var pr = seedRand(seed);
    var values = [];
    for (var vi = 0; vi < COUNT; vi += 1) values.push(30 + pr() * 220);
    var radii = values.map(function (v) { return Math.sqrt(v) * K; });
    var packCenterX = CALC_W * 0.5;
    var packCenterY = CALC_H * 0.5;
    var initialPositions = [];
    for (var pi = 0; pi < COUNT; pi += 1) {
      initialPositions.push({
        x: packCenterX + (pr() - 0.5) * 46,
        y: packCenterY + (pr() - 0.5) * 46
      });
    }
    var snapshots = [initialPositions.map(function (p) { return { x: p.x, y: p.y }; })];
    var relaxed = initialPositions.map(function (p) { return { x: p.x, y: p.y }; });
    for (var iter = 0; iter < BUBBLE_PACK_COLLISION_ITERATIONS; iter += 1) {

04番は累積面積25.8%・平均強度117.6、18/23 フレームで動きました。

05四分位数を箱とひげで描くボックスプロット

値を並べ替えて四分位の境界を計算し、箱の高さとひげの長さに変換して描くことで、中央値とばらつきが一つの図に収まります。グループ別レスポンスタイム分布の比較のように、平均だけでなくばらつきも重要な場面に向いています。

四分位数を箱とひげで描くボックスプロット
新しいタブで開く

05 · 四分位数を箱とひげで描くボックスプロット — 計測 累積面積 12.065% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GROUP_A_1quartileslerpStats
  var GROUP_A_1 = sample(9), GROUP_A_2 = sample(9);
  var GROUP_B_1 = sample(9), GROUP_B_2 = sample(9);

  function quartiles(arr) {
    var s = arr.slice().sort(function (a, b) { return a - b; });
    function pct(p) {
      var idx = (s.length - 1) * p, lo = Math.floor(idx), hi = Math.ceil(idx);
      return s[lo] + (s[hi] - s[lo]) * (idx - lo);
    }
    return { min: s[0], q1: pct(0.25), med: pct(0.5), q3: pct(0.75), max: s[s.length - 1] };
  }
  var qA1 = quartiles(GROUP_A_1), qA2 = quartiles(GROUP_A_2);
  var qB1 = quartiles(GROUP_B_1), qB2 = quartiles(GROUP_B_2);

  function lerp(x0, x1, f) { return x0 + (x1 - x0) * f; }
  function lerpStats(s1, s2, f) {
    return {
      min: lerp(s1.min, s2.min, f),
      q1: lerp(s1.q1, s2.q1, f),
      med: lerp(s1.med, s2.med, f),
      q3: lerp(s1.q3, s2.q3, f),
      max: lerp(s1.max, s2.max, f)
    };
  }

05番は累積面積12.1%・平均強度128.2、20/23 フレームで動きました。

06複数の軸を横切る線で値をつなぐ平行座標チャート

一つの項目が持つ複数の値を各縦軸上の座標として計算し線分でつなぎ、軸の一つをドラッグでなぞるとその区間を通る線だけが強調されます。多変量の商品スペック比較表に向いていて、この9つの中で09番と並んでArrowキー入力にも反応する2つのうちの一つでもあります。

複数の軸を横切る線で値をつなぐ平行座標チャート
新しいタブで開く

06 · 複数の軸を横切る線で値をつなぐ平行座標チャート — 計測 累積面積 46.867% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BRUSH_AXIS_INDEXsetBrushCenterautoDemoStep
  function setBrushCenter(v) {
    brushCenter = clamp(v, BRUSH_HALF, 1 - BRUSH_HALF);
    render();
  }

  canvas.addEventListener('pointerdown', function (e) {
    var rect = canvas.getBoundingClientRect();
    var x = (e.clientX - rect.left) * (cw / rect.width);
    var axisPx = axisX(BRUSH_AXIS_INDEX);
    if (Math.abs(x - axisPx) < cw * 0.14) {
      dragging = true;
      var y = (e.clientY - rect.top) * (ch / rect.height);
      lastUserInputAt = elapsed;
      setBrushCenter(yToValue(y));
    }
  });
  canvas.addEventListener('pointermove', function (e) {
    if (!dragging) return;
    var rect = canvas.getBoundingClientRect();
    var y = (e.clientY - rect.top) * (ch / rect.height);
    lastUserInputAt = elapsed;
    setBrushCenter(yToValue(y));
  });
  canvas.addEventListener('pointerup', function () { dragging = false; });
  canvas.addEventListener('pointerleave', function () { dragging = false; });

06番は累積面積46.9%・平均強度33.6と9つの中で最も広い面積が動き、21/23 フレームで動きました——ブラシをドラッグするたびキャンバス上の全ての線が同時に再描画されるためです。

07値ごとに半径が異なる花びらで広がるコクスコムチャート

項目数ぶん角度を均等に分け扇形を描き、値の大きさを半径に変換することで、同じ幅でも値が大きいほど花びらが外側まで伸びます。月別繰り返し指標比較のダッシュボードカードのように、各カテゴリの見た目の重みを揃えたまま長さだけで比較したい場面に向いています。

値ごとに半径が異なる花びらで広がるコクスコムチャート
新しいタブで開く

07 · 値ごとに半径が異なる花びらで広がるコクスコムチャート — 計測 累積面積 19.5% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

COXCOMB_PETAL_GAP_DEGtargetRadiiradiusAt
  var COXCOMB_PETAL_GAP_DEG = 2;
  var anglePerPetal = (360 - COXCOMB_PETAL_GAP_DEG * COUNT) / COUNT;

  var MIN_R = 18, MAX_R = 130;
  var maxValue = Math.max.apply(null, VALUES);
  var targetRadii = VALUES.map(function (v) { return MIN_R + (v / maxValue) * (MAX_R - MIN_R); });

  function easeOutCubic(x) { return 1 - Math.pow(1 - x, 3); }
  function easeInCubic(x) { return x * x * x; }
  function clamp01(x) { return Math.max(0, Math.min(1, x)); }

  var CENTER_X = CALC_W * 0.5, CENTER_Y = CALC_H * 0.5;
  var FILL_COLORS = ['47,109,246', '194,54,26', '43,157,116', '138,92,214'];
  var INK_STROKE = '23,20,26';

  function radiusAt(i, t) {
    var target = targetRadii[i];
    if (t < 1400) {
      var growProgress = clamp01((t - i * 90) / 500);
      return target * easeOutCubic(growProgress);
    }
    if (t < 1750) return target;
    var shrinkProgress = clamp01((t - 1750) / 200);
    return target * (1 - easeInCubic(shrinkProgress));
  }

07番は累積面積19.5%・平均強度137.9、17/23 フレームで動きました。

08順位が入れ替わると線が交差するバンプチャート

区間ごとに値の順序を付け直し順位を縦座標として計算し、二つの項目の順位が入れ替わる地点で線同士が交差します。四半期ごとの人気商品順位変動表に向いていて、線が交わる地点そのものが伝えたい情報になります。

順位が入れ替わると線が交差するバンプチャート
新しいタブで開く

08 · 順位が入れ替わると線が交差するバンプチャート — 計測 累積面積 42.761% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ranksOfPERIOD_COUNTSEG_DUR
  var rand = seedRand(425081);
  var ITEM_COUNT = 5, PERIOD_COUNT = 5;
  var VALUES = [];
  for (var p = 0; p < PERIOD_COUNT; p += 1) {
    var row = [];
    for (var i = 0; i < ITEM_COUNT; i += 1) row.push(10 + rand() * 90);
    VALUES.push(row);
  }
  function ranksOf(row) {
    var idx = row.map(function (v, i) { return i; });
    idx.sort(function (a, b) { return row[b] - row[a]; });
    var rank = new Array(row.length);
    idx.forEach(function (itemI, pos) { rank[itemI] = pos; });
    return rank;
  }
  var RANKS = VALUES.map(ranksOf);
  var LINE_COLORS = ['#2f6df6', '#c2361a', '#2b9d74', '#8a5cd6', '#0a4fe8'];

  var LOOP_MS = 2000;
  var T0 = 200;
  var SEG_MS = 300;
  var SEG_DUR = 260;
  var SEG_COUNT = PERIOD_COUNT - 1;

08番は累積面積42.8%・平均強度75.8、20/23 フレームで動きました。

09実体とひげで四本値を描くローソク足チャート

始値と終値の間を実体の四角形として、高値・安値までを細いひげ線として計算して描き、一本の棒にその日の四つの値が重なって収まります。コードで作る株価・為替の履歴パネルに向いていて、1本の足に4つの数値を同時に読ませられるのが強みになります。

実体とひげで四本値を描くローソク足チャート
新しいタブで開く

09 · 実体とひげで四本値を描くローソク足チャート — 計測 累積面積 13.603% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CANDLE_REVEAL_STAGGER_MSgrowPnearestCandle
  function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
  function easeOutCubic(x) { return 1 - Math.pow(1 - x, 3); }

  function candleX(idx) {
    var padX = cw * 0.06;
    var slot = (cw - padX * 2) / CANDLE_COUNT;
    return padX + slot * (idx + 0.5);
  }
  function priceY(v) {
    var padY = ch * 0.12;
    return ch - padY - (ch - padY * 2) * (v - scaleMin) / (scaleMax - scaleMin);
  }
  function growP(idx, t) {
    return clamp((t - T0 - idx * CANDLE_REVEAL_STAGGER_MS) / CANDLE_REVEAL_DUR_MS, 0, 1);
  }
  function nearestCandle(x) {
    var best = 0, bestD = Infinity;
    for (var k = 0; k < CANDLE_COUNT; k += 1) {
      var d = Math.abs(candleX(k) - x);
      if (d < bestD) { bestD = d; best = k; }
    }
    return best;
  }

09番は累積面積13.6%・平均強度121.0で、測定した23フレームすべてで動きました——9つの中で唯一23/23を記録しています。

値が描画結果を変える場所 — リボンの太さ・円の半径・花びらの角度

これら9つはどれも決まった形にラベルだけ差し替えているわけではなく、形そのものが値の関数になっています。01番のSANKEY_DRAW_DURATION_MSで描かれるリボンの太さはFLOWから来ていて、04番のradiiはMath.sqrt(v) * Kで計算されているため半径ではなく円の面積が値に比例し、07番のradiusAt(i, t)はVALUESをMIN_R〜MAX_Rの範囲に変換したtargetRadiiをそのまま読んでいます。各キャンバスを動かすrequestAnimationFrameのループ(MDNのWindow.requestAnimationFrame()参照)が決めているのは形がどう変化していくかだけで、行き着く先の値は常にデータから来ます。下の表は、各キャンバスが実際にどれだけ塗り直されているかを測定した結果です。

パターン 累積面積 平均強度 動いたフレーム
01 サンキーリボン 31.6% 88.9 19/23
02 ウォーターフォールブリッジ 10.0% 171.7 22/23
03 ファネルの先細り 29.8% 40.7 21/23
04 バブルパックの衝突 25.8% 117.6 18/23
05 ボックスプロットのひげ 12.1% 128.2 20/23
06 平行座標のブラシ 46.9% 33.6 21/23
07 コクスコムの花びら 19.5% 137.9 17/23
08 バンプチャートの順位交差 42.8% 75.8 20/23
09 ローソク足OHLC 13.6% 121.0 23/23

どこで壊れるか — 動作を減らす設定・狭い画面・値が0や負のとき

9つ全部が自動再生の前にwindow.matchMedia('(prefers-reduced-motion: reduce)')を確認しているので、この設定を有効にしている読者に動くキャンバスを強制する項目は一つもありません——ただし矢印キー入力にも対応しているのは9つのうち06番と09番の2つだけで、残り7つはポインターか自動再生のみに反応します。狭い画面でも、9項目すべてのfit()関数がwrap.getBoundingClientRect()を読んでキャンバスをラッパーに合わせて描き直すので絵自体は必ずラッパー幅いっぱいに収まりますが、09番のOHLCラベルのチップだけはctx.font = '11px sans-serif'で固定されているため、スマートフォン幅のデモではラベルが指しているローソク足に重なりやすくなります(340px以下では09番を除く8項目のキャプション文字が10pxに縮小されます)。このfit()関数の実装自体は、ラッパー要素を指すクラス名が違うだけで9項目ともほぼ同じ内容になっています。09番だけがこの縮小ルールの外に置かれているのは、ラベルをDOM要素ではなくキャンバスに直接ctx.fillTextで描いているためで、残り8項目のキャプションはDOM要素のテキストノードなので@mediaの指定がそのまま効く構造の違いによるものです。z7mev6vs、値が0や負のときの扱いは9項目で揃っていない点にも注意が必要で、実データを入れる前に知っておく価値があります——02番のウォーターフォールはb.value >= 0 ? COLOR_UP : COLOR_DOWNとはっきり分岐しているので負の増減は意図どおりに塗られますが、03番のdropRateはvalues[i - 1]で割る際にゼロ除算のガードがなく、値がちょうど0の段階があると離脱率のラベルが非有限値になり、09番のpriceY()も(scaleMax - scaleMin)で割っているため、すべてのローソク足の高値と安値が同じ値になるような変化のないデータでは値の幅がゼロになって縦軸のスケールが壊れます。

変形3つとアクセシビリティ

変形 定数と値 効果
サンキーのリボン描画速度を調整する SANKEY_DRAW_DURATION_MS 900 → 1800 値を小さくするとリボンが一気に速く描かれ、値を大きくするとリボンがゆっくり枝分かれしながら流れを追うように描かれます。
バブルパックの衝突反復回数を調整する BUBBLE_PACK_COLLISION_ITERATIONS 30 → 80 値を小さくすると円は多少重なったまま素早く落ち着き、値を大きくすると重なりが完全に解けるまで少しずつ長く位置を調整し続けます。
コクスコムの花びら間隔角度を調整する COXCOMB_PETAL_GAP_DEG 2 → 10 値を小さくすると花びらはほぼ隙間なく一枚の円盤のように見え、値を大きくすると花びらの間が開いて各片がはっきり分かれて見えます。

このセットのキャンバスはすべてCanvasRenderingContext2D APIだけで作られていて、チャートライブラリもWebGLコンテキストも使っていないので、アクセシビリティ対応は9つのindex.htmlが自分でやっていることがすべてになります。9つ中9つがprefers-reduced-motion: reduceのもとで自動再生ループを止め、永遠にループし続ける代わりに静止した最終フレームに落ち着きます——CSSトランジションと違ってキャンバスの再描画は自分では止まらないので、この分岐はここでは特に重要になります。矢印キーによるkeydownハンドラーを追加しているのは06番(ブラシ用のArrowUp/ArrowDown)と09番(ローソク足カーソル用のArrowLeft/ArrowRight)の2つだけで、残り7つはマウスやタッチでのドラッグに相当する操作をキーボードから呼び出す手段自体を持たない自動再生専用のデモになっています。

グラフ 種類 使い分けは何が基準か?

チャートライブラリを使えば、この9つの描き方を全部ひとつの設定オブジェクトの裏に隠したまま使えてしまうが、そのせいでそれぞれが数値に対して実際に何をしているのかが逆に見えにくくなります。生のキャンバス計算まで分解すると、01番のリボンの太さも、05番の四分位ボックスも、08番の順位交差点も、それぞれが配列からピクセルへの変換をひとつずつ行っているだけで、その変換を読むことこそが見た目の派手さではなく実データにどのグラフ 種類が合うかを判断する一番早い方法になります。css グラフ をSVGで描く9種 — ライブラリなしはこの中の5つの表現をキャンバスのピクセルではなくSVGのpathデータで描いていて、どんな拡大率でも輪郭を崩したくないときはこちらを選ぶことになり、ダッシュボード グラフ デザイン 操作9選 — 触ると数字が変わるはこのセットが止まったところから先——完成したチャートをドラッグ・ブラシ・フィルターできるものに変える話を引き継ぎます。流れと段階を扱う前半のほうが今の用途に近いなら、JavaScript グラフ描画 9選 — ライブラリなしのCanvasアニメーションがこのセットとは重ならない別の9種類をやはりライブラリなしのキャンバスで描いています。

ダッシュボードに組み込む前のFAQ

サンキーダイアグラム 作り方を知らなくても01番のコードはそのまま使えますか?

buildLayout()はFLOWの中身がどこから来たかを気にせず同じ計算をするだけなので、乱数で作ったダミーの流量を実際のデータに置き換えるだけで動きます。リボンの太さの計算式自体を書き換える必要はありません。

02番はなぜ棒の値をそのまま描かずanchorを使うのですか?

ウォーターフォールの棒は0からではなく前の棒が終わった高さから始まる必要があるためで、anchorはその累積値を保持していて、伸びるアニメーションが正しい床から立ち上がるようにしています。床の値がずれると棒同士のつながりが橋として見えなくなります。

04番のバブルパックは重なりが完全になくなるまで計算されますか?

既定値のBUBBLE_PACK_COLLISION_ITERATIONSが30のときは必ずしもそうではなく、これは「重なりが消えるまで」ではなく固定回数のループなので、項目が密集したデータではループが終わった時点でわずかに重なりが残ることがあります。2つ目の変形のように80まで増やすと、円が完全に離れるまでの調整回数が増えます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。