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

グラフ アニメーション css ピッカー9パターン

バーをピクセル位置だけで動かすとグラフが値とずれる — だから値ひとつで棒の高さを再計算するピッカー、比率で弧の長さを満たすゲージピッカー、通った値を折れ線に残すスパークラインピッカーまで九つを見る。これが グラフ アニメーション css の実例です。

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

九つとも、サーバーからも時計からも localStorage からも値を読みません。ライブラリもWebGLもCDNも読み込みません。ヒストグラムの十二本の基準高さ、レーダーの五軸の基準値、波形の四十本のバーまで、すべて読み込み時に一度だけ決まる固定配列で、ドラッグで動くのはその配列と現在値から導かれる形だけです。軸やレジェンドのラベルもわざと中立的(Axis 1〜Axis 5、バケット番号)にしてあるので、これは グラフ アニメーション 作り方 を探していてダッシュボード用でも設定パネル用でも同じように読めます。

ダッシュボードの再生・値だけのピッカーと、何が違うか

264番のダッシュボードグラフは読み込み時やデータ更新時にアニメーションしますが、数値はすでに存在していて、アニメーションはそれを見せているだけです。224番のピッカーはつまみやスウォッチを動かして数値や色を更新しますが、ページ上にグラフそのものはありません。この記事の九つはその中間にあります — つまみをドラッグするのが唯一の入力で、グラフそのもの(棒の高さ、弧の長さ、折れ線の頂点)がその一つの数値から毎フレーム再計算されます。ドラッグの前には何も存在しないので、見せるものではなく作るものです。

ポインターが動くたびに何が再計算されるか

九つとも同じ入力面を持ちます — ドラッグ用の pointerdown/pointermove/pointerup、矢印キーで動かす keydown、描画枠とドラッグ帯を合わせ続ける resize です。移動が受理されるたびに requestAnimationFrame のコールバックが現在値を読み、そのパターン専用の再計算関数を一つ呼びます — ヒストグラムならバケット番号と充填率、ゲージとオービットダイヤルなら弧の角度、スパークラインならトレイル配列のシフト、ドーナツとツリーマップならセグメントの再分割、レーダーなら五本の軸長、波形ならアクティブなバーの境界、ヒートマップなら色の比率です。続くDOM更新は常に数値を反映する最小限のもの — <rect> の高さを数本、<path d> を一本、クラスの切り替えを数個で、SVGやcanvas全体を描き直すことはありません。

九パターンを、動いた面積とフレーム数で比べる

以下の数値はデモを見た印象ではなく、run/398/demo/0N-*/측정.json に書かれた実測値です — このピラーの他の記事と同じピクセル差分パスで、各パターンのドラッグ範囲全体を描画し、連続するフレームを差分して480×300のキャンバスのうち変化した割合と、変化した箇所の平均強度を出しています。スパークライン(チャート幅のほとんどで折れ線が描き直る)とレーダー(中心付近の短いセグメントだけ)でこれほど差が出るのは、CSSから推測しているのではなく、実際に動いたピクセルを測っているからです。

# パターン 累積面積 平均強度 動いたフレーム
01 ヒストグラムのバケット 23.2% 145.2 23/23
02 ラジアルゲージの弧 10.0% 137.6 23/23
03 スパークライン折れ線 32.8% 167.5 23/23
04 ドーナツ再分割 12.2% 150.1 23/23
05 レーダー多角形 9.6% 155.4 23/23
06 波形スクラブ 23.1% 174.5 23/23
07 オービットダイヤルの弧 9.6% 187.7 23/23
08 ヒートマップのセル 15.6% 73.7 23/23
09 ツリーマップのブロック 24.6% 117.4 23/23

九つとも、ドラッグ範囲を通した23回のフレーム比較すべてで動いています — トラックの端だけでアニメーションするパターンはありません。

パターン別コード — index.htmlからそのまま切り出したもの

以下の各ブロックは、その項目の index.html からそのまま取り出したもので、簡略化した書き直しではありません — ライブデモのコードパン式タブ(HTML・CSS・JS)が見せているのと同じコードです。グラフ アニメーション web の実装を探しているなら、CSSトランジションだけでなく再計算関数まで含めて確認できます。

01ヒストグラムのバケットが一つの値の周りで再分配される

ドラッグで0〜100の値が動き、ピッカーはそれを十個あるバケットのどれかに割り当て、そのバケットの目標高さだけが変わります — 残り九個は基準の高さのままです。

ドラッグごとに棒の高さが再計算されるヒストグラムピッカー
新しいタブで開く

01 · ドラッグごとに棒の高さが再計算されるヒストグラムピッカー — 計測 累積面積 23.203% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BUCKET_COUNTPEAK_BOOSTCHART_H
function bucketOf(v) {
  var idx = Math.floor(v / 100 * BUCKET_COUNT);
  return Math.max(0, Math.min(BUCKET_COUNT - 1, idx));
}

function fracIn(v, bucket) {
  var raw = v / 100 * BUCKET_COUNT - bucket;
  return Math.max(0, Math.min(1, raw));
}

// 値が属する区間だけ目標の高さを再計算する — 残りは基準の高さのまま
function recompute() {
  var b = bucketOf(value);
  var f = fracIn(value, b);
  for (var i = 0; i < BUCKET_COUNT; i++) {
    targetH[i] = BASE[i];
  }
  targetH[b] = Math.min(0.96, BASE[b] + PEAK_BOOST * (0.4 + 0.6 * f));
  return b;
}

02ラジアルゲージの弧の長さが比率を追う

塗りつぶし弧の終端角度は比率の一次関数なので、ドラッグで変わるのは弧の回転ではなく弧の長さそのものです。

値の分だけ弧が満ちる放射状ゲージピッカー
新しいタブで開く

02 · 値の分だけ弧が満ちる放射状ゲージピッカー — 計測 累積面積 9.973% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

STARTENDMath.atan2
function arcPath(cx, cy, r, a0, a1) {
  if (Math.abs(a1 - a0) < 0.5) return '';
  var s = polar(cx, cy, r, a0), e = polar(cx, cy, r, a1);
  var large = (a1 - a0) > 180 ? 1 : 0;
  return 'M ' + s.x + ' ' + s.y + ' A ' + r + ' ' + r + ' 0 ' + large + ' 1 ' + e.x + ' ' + e.y;
}

function angleOf(v) { return START + (SWEEP * v) / 100; }

function paint() {
  track.setAttribute('d', arcPath(CX, CY, R, START, END));
  fill.setAttribute('d', arcPath(CX, CY, R, START, angleOf(current)));
  var p = polar(CX, CY, R, angleOf(current));
  grab.style.left = (p.x / 480 * 100) + '%';
  grab.style.top = (p.y / 300 * 100) + '%';
  var rv = Math.round(current);
  numEl.textContent = rv + '%';
  valEl.textContent = String(rv);
  grab.setAttribute('aria-valuenow', String(rv));
  grab.setAttribute('aria-valuetext', 'レベル ' + rv + '%');
}

03スパークラインの折れ線が通った値をすべて残す

受理されたドラッグ位置は毎回固定長のトレイル配列に積まれ、折れ線は毎フレームその配列から組み立て直されます — 一点だけでなく短い履歴が線の形になります。この グラフ アニメーション 折れ線 の作り方は、単に点を打つのではなく配列全体を引き直す点が要です。

ドラッグの軌跡が折れ線として残るスパークラインピッカー
新しいタブで開く

03 · ドラッグの軌跡が折れ線として残るスパークラインピッカー — 計測 累積面積 32.755% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

TRAIL_LENGTHCHART_WSTEP
function buildPoints() {
  var step = CHART_W / (TRAIL_LENGTH - 1);
  var pts = [];
  for (var i = 0; i < TRAIL_LENGTH; i++) pts.push([i * step, yOf(trail[i])]);
  return pts;
}

function paint() {
  var pts = buildPoints();
  var d = 'M ' + pts[0][0] + ' ' + pts[0][1];
  for (var i = 1; i < pts.length; i++) d += ' L ' + pts[i][0] + ' ' + pts[i][1];
  lineEl.setAttribute('d', d);
  areaEl.setAttribute('d', d + ' L ' + pts[pts.length - 1][0] + ' ' + CHART_H + ' L ' + pts[0][0] + ' ' + CHART_H + ' Z');
  var last = pts[pts.length - 1];
  dotEl.setAttribute('cx', String(last[0]));
  dotEl.setAttribute('cy', String(last[1]));
  var rv = Math.round(current);
  valEl.textContent = String(rv);
  grab.style.left = value + '%';
  grab.setAttribute('aria-valuenow', String(Math.round(value)));
  grab.setAttribute('aria-valuetext', '値 ' + Math.round(value));
}

04ドーナツの再分割が三つのセグメントの比率を保つ

一つのセグメントがドラッグした比率まで伸びると、残り三つは縮みますが、固定の比率配列を使って互いの相対サイズは保たれます。

値の比率だけ断片が伸びるドーナツピッカー
新しいタブで開く

04 · 値の比率だけ断片が伸びるドーナツピッカー — 計測 累積面積 12.204% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LABELSOTHER_RATIOSTROKE
function polar(cx, cy, r, deg) {
  var rad = deg * Math.PI / 180;
  return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
}

function arcPath(cx, cy, r, a0, a1) {
  if (a1 - a0 < 0.6) return '';
  var s = polar(cx, cy, r, a0), e = polar(cx, cy, r, a1);
  var large = (a1 - a0) > 180 ? 1 : 0;
  return 'M ' + s.x + ' ' + s.y + ' A ' + r + ' ' + r + ' 0 ' + large + ' 1 ' + e.x + ' ' + e.y;
}

function recompute(v) {
  var rest = 100 - v;
  pct = [v, rest * OTHER_RATIO[0], rest * OTHER_RATIO[1], rest * OTHER_RATIO[2]];
}

05レーダーの多角形が五軸まとめて描き直される

ポインターが触れているのは一軸だけですが、多角形のパスは毎フレーム五軸すべての長さから組み立て直されるので、形全体が一点の変化を中心に目に見えて動きます。

軸ごとに点を引くと形が再描画されるレーダーピッカー
新しいタブで開く

05 · 軸ごとに点を引くと形が再描画されるレーダーピッカー — 計測 累積面積 9.604% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

AXIS_COUNTPHASESPERIODS
function paint() {
  var d = '';
  for (var i = 0; i < AXIS_COUNT; i++) {
    var p = polar(CX, CY, R * (current[i] / 100), angleOf(i));
    d += (i === 0 ? 'M ' : 'L ') + p.x + ' ' + p.y;
    pointEls[i].setAttribute('cx', String(p.x));
    pointEls[i].setAttribute('cy', String(p.y));
  }
  polyEl.setAttribute('d', d + ' Z');
  var rv = Math.round(current[0]);
  valEl.textContent = String(rv);
  grab.style.left = ((values[0] - MIN_V) / (MAX_V - MIN_V) * 100) + '%';
  grab.setAttribute('aria-valuenow', String(Math.round(values[0])));
  grab.setAttribute('aria-valuetext', 'A軸 ' + Math.round(values[0]));
  var rows = '';
  for (var j = 0; j < AXIS_COUNT; j++) rows += '<tr><th>' + LABELS[j] + '</th><td>' + Math.round(current[j]) + '</td></tr>';
  srBody.innerHTML = rows;
}

06波形スクラブが再生済みのバーに印をつける

ドラッグ位置はバー番号に対応し、その番号より前のバーはすべて is-past クラスがつきます — バーが伸び縮みするのではなく、固定された波形の上を再生ヘッドが動くように見えます。

スクラブした位置までバーが満ちる波形ピッカー
新しいタブで開く

06 · スクラブした位置までバーが満ちる波形ピッカー — 計測 累積面積 23.116% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BAR_WHEIGHTSGAP
function buildBars() {
  var html = '';
  for (var i = 0; i < N; i++) {
    var h = HEIGHTS[i] * CHART_H;
    var x = i * BAR_W + GAP / 2;
    var w = Math.max(1, BAR_W - GAP);
    html += '<rect class="wf__bar" id="wb' + i + '" x="' + x + '" y="' + (CHART_H - h) + '" width="' + w + '" height="' + h + '"></rect>';
  }
  barsG.innerHTML = html;
  for (var j = 0; j < N; j++) barEls.push(document.getElementById('wb' + j));
}

function paint() {
  var activeIdx = Math.round((current / 100) * N);
  for (var i = 0; i < N; i++) barEls[i].classList.toggle('is-past', i < activeIdx);
  var x = (current / 100) * 480;
  headline.setAttribute('x1', String(x));
  headline.setAttribute('x2', String(x));
  var rv = Math.round(current);
  valEl.textContent = String(rv);
  grab.style.left = value + '%';
  grab.setAttribute('aria-valuenow', String(Math.round(value)));
  grab.setAttribute('aria-valuetext', '再生位置 ' + Math.round(value) + '%');
}

07オービットダイヤルの弧が生の角度より遅れる

点の目標角度はポインターに合わせて即座に飛びますが、描画される弧の終端は毎フレーム ARC_LERP の分だけそこへ近づくので、弧が点に対して目に見えて遅れて追いかけます。

値が軌道を回る長さに変わるオービットダイヤルピッカー
新しいタブで開く

07 · 値が軌道を回る長さに変わるオービットダイヤルピッカー — 計測 累積面積 9.642% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ARC_LERPMath.atan2clip-path
function arcPath(cx, cy, r, a0, a1) {
  if (a1 - a0 < 0.6) return '';
  var s = polar(cx, cy, r, a0), e = polar(cx, cy, r, a1);
  var large = (a1 - a0) > 180 ? 1 : 0;
  return 'M ' + s.x + ' ' + s.y + ' A ' + r + ' ' + r + ' 0 ' + large + ' 1 ' + e.x + ' ' + e.y;
}

function angleOf(v) { return START + (v / 100) * 360; }

function paint() {
  var deg = angleOf(current);
  arcEl.setAttribute('d', arcPath(CX, CY, R, START, deg));
  var p = polar(CX, CY, R, deg);
  dotEl.setAttribute('cx', String(p.x));
  dotEl.setAttribute('cy', String(p.y));
  grab.style.left = (p.x / 480 * 100) + '%';
  grab.style.top = (p.y / 300 * 100) + '%';
  var rd = Math.round(deg - START);
  valEl.textContent = String(rd);
  grab.setAttribute('aria-valuenow', String(Math.round(current)));
  grab.setAttribute('aria-valuetext', '軌道 ' + rd + '度');
}

08ヒートマップのセルが強度比率で色を変える

ドラッグした値は、静的なレジェンドセルを作るのに使ったのと同じ colorFor() を通して色比率に変換されるので、メインセルの色と固定レジェンドの色は常に一つの共通スケールから出ています。

値がセルの明るさに変わるヒートマップピッカー
新しいタブで開く

08 · 値がセルの明るさに変わるヒートマップピッカー — 計測 累積面積 15.633% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

RATIOSMAX_Vgrid-template-columns
function buildStatic() {
  var legendHtml = '';
  for (var i = 0; i < N; i++) {
    var cell = document.createElement('div');
    cell.className = 'hm__cell';
    cell.style.background = colorFor(RATIOS[i]);
    grid.appendChild(cell);
    legendHtml += '<li>固定マス ' + (i + 1) + ': ' + Math.round(RATIOS[i] * 100) + '%</li>';
  }
  legend.innerHTML = legendHtml;
}

function paint() {
  var ratio = current / MAX_V;
  hero.style.background = colorFor(ratio);
  var rv = Math.round(current);
  valEl.textContent = String(rv);
  grab.style.left = value + '%';
  grab.setAttribute('aria-valuenow', String(Math.round(value)));
  grab.setAttribute('aria-valuetext', '選択セル ' + Math.round(value) + '%');
}

09ツリーマップのブロックが残り幅から再分割される

ブロックAが伸びるとB・C・D用の列幅が縮みますが、その高さは幅だけでなく「残りからの再計算」で決まるので、一つのブロックが他を覆い隠すのではなく、レイアウト全体が再分割されます。

値の大きさだけブロックが分割し直されるツリーマップピッカー
新しいタブで開く

09 · 値の大きさだけブロックが分割し直されるツリーマップピッカー — 計測 累積面積 24.647% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

OTHER_RATIOMIN_Vclip-path
  var rest = 100 - a;
  var b = rest * OTHER_RATIO[0];
  var c = rest * OTHER_RATIO[1];
  var d = rest * OTHER_RATIO[2];

  blkA.style.width = a + '%';
  blkA.style.height = '100%';
  blkA.style.left = '0%';
  blkA.style.top = '0%';

  var colW = 100 - a;
  var bH = rest > 0.001 ? (b / rest) * 100 : 0;
  var cH = rest > 0.001 ? (c / rest) * 100 : 0;

値がゼロに近づくと弧が空のパスに壊れる場所

ラジアルゲージ・ドーナツ・オービットダイヤルの arcPath() はどれも、開始角と終了角の差がある閾値(ゲージは0.5度、ドーナツとオービットは0.6度)を下回ると、細い弧ではなく完全に空の文字列を返します — 長さゼロの弧でも一点でもなく、何も描かれません。これは実在する落とし穴です。ゲージの範囲の一番下やドラッグの始まったばかりの瞬間、塗り弧はCSSの stroke-dasharray トランジションのように細いスライバーへ縮んでいくのではなく、いきなり消えます。SVGの弧コマンドがほぼゼロのスイープを表現できず、端点の計算が退化するためです。この三つのどれかを「ほぼゼロでも見えたままにしたい」用途に転用するなら、閾値を小さくするのではなく、ガード節の手前に最小角度の下限を足すのが直し方です。

アクセシビリティ — prefers-reduced-motion が九つすべてで止めるもの

九つとも読み込み時に一度だけ prefers-reduced-motion を確認し、設定されていれば requestAnimationFrame のイージングを丸ごとスキップします — 値は目標へ直接飛び、paint()(と aria-live の読み上げ)は何フレームにも分けず一度だけ同期的に走ります。<span class="pw-inline" id="pw">spguctjy</span> zipの中でも各パターンの reduced-motion 分岐はそのまま残っているので、この経路を試すために作り直す必要はありません。すべてのダイヤルとつまみは aria-valuenow と aria-valuetext も持っていて、スパークラインなら「値 62」、オービットダイヤルなら「軌道 140度」とスクリーンリーダーに伝わります — グラフ自体には形の代替テキストがなくても、現在の数値は伝わります。

値で動くグラフピッカーを実際にどこで使うか

動画プレイヤーの設定画面には波形スクラブ(項目06)がいちばん合います — ただのrangeインプットではなく、再生済みの部分がトラックの形に対して見た目でわかるので、ユーザーがすでに知っている本物のプレイヤーのシークバーに近づきます。ヒストグラムのバケットピッカー(項目01)は「一つのセグメントだけを狙う」操作に向いていて、レポートのフィルターで価格帯や年齢層を選ぶときなど、他のバケットが動かないままなのを見せることで何も壊れていないと確認できます。スパークラインの折れ線(項目03)は音量や明るさのような、ライブで微調整するつまみに向いていて、現在地だけでなく直近の数回分を見せることで行き過ぎたり戻したりしていないかがわかります。三つとも、レポート用のダッシュボードというより、SVGの読み上げがついたスタイル済みの <input type="range"> に近い立ち位置です。

九つのファイルは同じまま、三つの変形

変形 変更点 感触
01 — バケット数 BUCKET_COUNT 12 → 24 一つひとつの区間が細くなり、ピークになっても視覚的な主張が弱くなる
03 — トレイル長 TRAIL_LENGTH 48 → 96 表示される履歴が長くなり、折れ線がよりゆっくりスクロールする軌跡に見える
07 — 弧のイージング ARC_LERP 0.18 → 0.08 弧が点に対してより目立って遅れ、重くもたついた感触に近づく

値をドラッグしてグラフが計算し直される前に、よくある疑問

九つのどれかは実データを読み込んだり、次回訪問まで値を保存したりしますか?

いいえ。すべての系列はスクリプトが実行された時に一度だけ代入される固定配列で、fetch も localStorage も Math.random も使っていません。ドラッグで変わるのは、その固定配列のどの部分を強調するか、どう要約するかだけです。

なぜスパークラインの累積面積(32.8%)はレーダー(9.6%)よりずっと高いのですか?

スパークラインの折れ線と塗り面はほぼチャート幅いっぱいで毎フレーム描き直りますが、レーダーはドラッグしている軸に最も近い五本の短いセグメントだけが描き直ります — 同じ480×300のキャンバスでも、フレームごとに変化する量がまったく違います。

九つ全部ではなく、一つだけ既存のプロジェクトに入れられますか?

はい。zipの中の各 index.html は自己完結していて(それぞれ専用の <style> と <script> を持ち、共有ファイルはありません)、一項目のマークアップ・CSS・JSだけを取り出せば十分です — どの項目も他の項目のIDやクラスを参照していません。

出典: MDN — requestAnimationFrame、MDN — prefers-reduced-motion、MDN — Pointer events。

関連記事: ダッシュボードのチャート演出、CSS プログレスバー・ゲージ集、CSS ピッカーUIパターン集。

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

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