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

JavaScript グラフ描画 9選 — ライブラリなしのCanvasアニメーション

javascript グラフ描画とは、データの配列を x・y 座標に置き換えて、線・棒・円弧として canvas に乗せていく作業のことです。ライブラリなしで、CDN なしで、値が引かれ、満ち、集まっていくグラフを九つ作りました。ファイルを開けばコピーしたまま動きます。

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

並び順は難しさではなく canvas にインクを置く道具がひとつずつ増えていく順 です。一本のストロークで始まった線(01)が矩形の塗りを手に入れて棒になり(02)、円弧を手に入れてドーナツになります(03)。同じ平面に点を40個撒けば散布図(04)、塗った面を二枚重ねれば面グラフ(05)です。続く四つはそのインクをどう動かすかです — 針が一本、減衰振動しながら値を語り(06)、時間窓が押されて五本のミニ線が生きて呼吸し(07)、多角形が頂点ごとに違う速度で変形し(08)、最後に56個のセルが波になって満ちていきます(09)。ピクセルを計算する側に興味があるなら canvas 特殊効果 9種 で水滴とブラックホールを、ベクターで描く側は css グラフ をSVGで描く9種 で、すでに描かれたグラフをマウスで触る側は ダッシュボード グラフ デザイン 操作 9選 で扱っています。

01折れ線グラフの描画 — クリップが開いて現れる12週間

画面に見えるもの

再描画ボタンを押すと、12週間の売上 [128, 176, 142, 210, 248, 204, 292, 336, 288, 372, 414, 468] が左から右へひと筆で引かれ、ペンが通った頂点には紙色のマーカーが打たれ、最後に端点が半径2.6から5.5へ膨らんで止まります。

折れ線グラフの描画
新しいタブで開く

01 · 折れ線グラフの描画 — 計測 累積面積 20.228% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

quadraticCurveTocliplineTo

計算の見どころ

2秒のループで動いたフレームは23/24、累積面積は20.228パーセントを記録しました。週次・月次レポートの最初の行に合う絵です — 進行 p を11個の週区間に載せて現在の辺と端点座標を求めるのがこのアニメーションの心臓部です。

// 描画の進行 p(0..1) → { i, k, ex, ey, f }
//   f = 0..11 の正規化経過、i・k = 現在の辺、(ex,ey) = 端点座標
function headAt(p) {
  var n = WEEKS.length;
  var f = Math.min(1, p / DRAW_AT) * (n - 1);
  var i = Math.min(n - 2, Math.floor(f));
  var k = f - i;
  var ex = xAt(i) + (xAt(i + 1) - xAt(i)) * k;
  var ey = yAt(WEEKS[i]) + (yAt(WEEKS[i + 1]) - yAt(WEEKS[i])) * k;
  return { i: i, k: k, f: f, ex: ex, ey: ey };
}
// 通過した頂点を制御点にして、次の頂点との**中点**まで quadraticCurveTo で押さえる(頂点をかすめるように)
function tracePath(p) {
  var d = headAt(p);
  ctx.beginPath();
  ctx.moveTo(xAt(0), yAt(WEEKS[0]));
  for (var j = 1; j < d.i; j++) {
    ctx.quadraticCurveTo(xAt(j), yAt(WEEKS[j]), (xAt(j) + xAt(j + 1)) / 2, (yAt(WEEKS[j]) + yAt(WEEKS[j + 1])) / 2);
  }
  if (d.i >= 1) {
    ctx.quadraticCurveTo(xAt(d.i), yAt(WEEKS[d.i]), d.ex, d.ey);
  } else {
    ctx.lineTo(d.ex, d.ey);
  }
  return d;
}

頂点を制御点にして隣の頂点との中点まで曲線を押さえるこのやり方は頂点をかすめて通るので階段のように跳ねず、データの位置は正確に守られ、クリッピング矩形が同じ速度で開いて線が通った順に現れます。

02棒グラフの成長 — 時間差で広がる波

画面に見えるもの

検索420・ソーシャル310・メール265・直接195・パートナー140(万ウォン)の五本の棒が全体の7.5パーセントの間隔で時間差を置いて足元から伸びて波になり、着地した棒から順に値ラベルがカウントアップされます。

棒グラフの成長
新しいタブで開く

02 · 棒グラフの成長 — 計測 累積面積 14.631% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

staggereaseOutCubicfillRect

計算の見どころ

動いたフレーム23/24・累積面積14.631パーセント・強度の平均118.5を記録しました。チャネルや拠点を比べるダッシュボードに置く絵です — 誰もが思い浮かべるこの形が canvas では fillRect 四つの引数の計算だとコードが示します。

// 五本の棒 — i 番目は i*STAGGER に始まり GROW の間伸びる(時間差の波)
for (var i = 0; i < n; i++) {
  var row = ROWS[i];
  var cx = PAD_L + slot * (i + 0.5);
  var fullH = plotH * (row.v / 450);
  var local = Math.max(0, Math.min(1, (p - i * STAGGER) / GROW));
  var eased = easeOutCubic(local);
  var h = fullH * eased;
  var y = baseY - h;
  ctx.fillStyle = row.c;
  ctx.fillRect(cx - barW / 2, y, barW, h);
}

一本が伸びる区間 GROW は全体2秒の46パーセント、時間差 STAGGER は7.5パーセントなので最後の棒は0.30地点から出発し、幅は min(44, max(18, slot * 0.62)) で上下限を挟んでチャネルが増えても櫛のように細りすぎないようにしました。

03ドーナツのスイープ — 終わり角から始まる次のスライス

画面に見えるもの

検索10,800(45パーセント)・ソーシャル8,400(35パーセント)・直接4,800(20パーセント)の三つのスライスが12時方向からつながって止まらず流れるように満ち、リングが埋まったあと中央の「充填中」が合計訪問24,000へ縦スライドで差し替わります。

ドーナツのスイープ
新しいタブで開く

03 · ドーナツのスイープ — 計測 累積面積 11.843% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

arcstartAngleendAngle

計算の見どころ

動いたフレーム23/24・累積面積11.843パーセント・強度の平均169.0 — 九つの中で最も強い変化率を記録しました。流入経路や構成比を一目で見せる場所に置きます — 進行を占有率の合計に分配して、今のスライスの終わり角が次のスライスの開始角になる構造です。

var q = qTrack;
var pen = START;
var acc = 0;
var cur = -1;
for (var i = 0; i < SRC.length; i++) {
  var share = SRC[i].v / TOTAL;
  var segEnd = acc + share;
  if (q >= segEnd) {
    // すでに通過したスライス — 全体角を描く(両側に隙間を半分ずつ)
    var a0 = START + acc * TAU + GAP / 2;
    var a1 = START + segEnd * TAU - GAP / 2;
    slice(cx, cy, R, ring, a0, a1, SRC[i].c, 1);
    pen = a1;
    acc = segEnd;
  } else if (q > acc) {
    // 今描かれているスライス — 開始角は前のスライスの終わり角。終わり角は進行に従って開く
    cur = i;
    var a0c = START + acc * TAU + (i === 0 ? 0 : GAP / 2);
    var a1c = START + q * TAU;
    slice(cx, cy, R, ring, a0c, a1c, SRC[i].c, 1);
    acc = segEnd;
  } else {
    break;
  }
}

スライスの隙間は0.035ラジアンに固定しました — くっついていると境界が読めないからです。トラック(薄く敷いておく居場所)も最初のフレームで一気に敷かず、ペンが通った分だけ開きます。

04散布図の出現 — lerp で集まってくる40個

画面に見えるもの

40個の商品の点が散らばった位置から始まって繰り返し補間で価格(0.5〜19.5万ウォン)とレビュー数(10〜480)の平面の定位置へ集まり、マウスを載せると最寄りの点がリングで捕まり、商品名と価格・レビュー数が浮かびます。

散布図の出現
新しいタブで開く

04 · 散布図の出現 — 計測 累積面積 19.836% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

lerpMath.expdevicePixelRatio

計算の見どころ

動いたフレーム21/24・累積面積19.836パーセントを記録しました。価格と満足度のような相関を探る場所に置きます — 点ごとに収束速度 lam を3.0〜6.0に変えて与えるので、一斉に到着せず降り注ぐように止まります。

// 閉じた形の lerp 収束 — 経過時間 e(秒)から点の位置を決定的に計算して cx・cy に保存、進行度 t を返す
function converge(pt, e) {
  var t = 1 - Math.exp(-pt.lam * e);
  var x0 = PAD_L + (W - PAD_L - PAD_R) * pt.sx;
  var y0 = PAD_T + (H - PAD_T - PAD_B) * pt.sy;
  pt.cx = x0 + (px(pt.price) - x0) * t;
  pt.cy = y0 + (py(pt.rv) - y0) * t;
  return t;
}

40個の目標座標と開始座標はシード3050416のLCG(乗数48271・法2147483647)の呼び出し順が固定なので、誰がいつ開いても同じ絵が再現されます — 収束は1.6秒、ループ全体は2.4秒です。

05面グラフの塗り — 交差する月が明るくなる二系列

画面に見えるもの

訪問 [210, 248, 226, 292, 334, 306, 368, 412, 384, 446, 482, 456] と注文 [96, 118, 142, 176, 208, 236, 272, 308, 344, 386, 424, 468] の二系列の面が下から満ち、注文が訪問を追い越す11月から12月の区間では上に来た系列が一段明るくなり、塗り終えると平均線がその上を横切ります。

面グラフの塗り
新しいタブで開く

05 · 面グラフの塗り — 計測 累積面積 25.02% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

globalAlphaclipquadraticCurveTo

計算の見どころ

動いたフレーム23/24・累積面積25.02パーセントを記録しました。目標対実績や累積の推移に置きます — 減速イージングひとつでクリッピングの上昇と面の高さを一緒に押さえるのがこのデモの計算です。

// 塗りの進行率 — 減速イージングひとつでクリップと面の高さを一緒に押さえる
var f = easeOutCubic(Math.min(1, p / FILL_AT));
var s = f * f;                                   // 面の高さ — 始まりが急で終わりに張り付く重さ
var cutY = (H - PAD_B) - (H - PAD_T - PAD_B) * f;

ctx.save();
ctx.beginPath();
ctx.rect(0, cutY - 1, W, H);
ctx.clip();

areaPath(B, s);
ctx.fillStyle = C_2F;
ctx.fill();
areaPath(A, s);
ctx.fillStyle = C_1F;
ctx.fill();

塗りは全体2秒の85パーセント地点で終わり(FILL_AT)、平均線は93パーセント(LINE_AT)で確定し、面の高さは f * f でもう一段押さえて始まりが急で終わりに張り付く重さを出しました。

06ゲージの針 — 目標を通り越して戻る減衰振動

画面に見えるもの

値72を指す針が目標角を通り越してバネの減衰で引き返して止まり、目盛りの三区間(0-40-70-100)は針が触れた分だけ塗られ、値は最終角で確定します。

ゲージの針
新しいタブで開く

06 · ゲージの針 — 計測 累積面積 10.783% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

Math.cosarctextAlign

計算の見どころ

動いたフレーム21/24・累積面積10.783パーセント・強度の平均119.3を記録しました。KPIの達成率やサービスの状態表示に置きます — オーバーシュートをCSSのイージングではなく物理の式、減衰指数と角振動数で計算したのが特徴です。

// スイープの進行 u(0..1) → 針の値(0〜100超)。減衰サイン波で目標を通り越して戻る:
//   目標以上の成分 VALUE * (1 - e^(-DECAY*u) * cos(OMEGA*u)) — u=0 で 0、u→1 で VALUE へ収束
function needleVal(u) {
  return VALUE * (1 - Math.exp(-DECAY * u) * Math.cos(OMEGA * u));
}

// 値(0〜100) → canvas の角度(ラジアン)。canvas は y 軸が下に開いているので
// 上側の半円は PI(左) → 1.5PI(頂点) → 2PI(右) の順に回る — 時計回り(角度の増加)
function angOf(v) { return Math.PI + Math.PI * (v / 100); }
function pt(v, r) {
  var a = angOf(v);
  return { x: cx() + Math.cos(a) * r, y: cy() + Math.sin(a) * r };
}

減衰係数 DECAY 5.2と角振動数 OMEGA 10.5が針が何回揺れるかを決めます。値が大きくなるほど振幅が縮む指数減衰なので、最後の揺れはほとんど目に留まりません。

07スパークラインの流れ — 窓を押して生きる五枚のカード

画面に見えるもの

訪問者・滞在・注文・クーポン・閲覧の五枚の指標カードのミニ線が2秒のループの中で0.28・0.55・0.82の三地点で窓を押して新しい値を受け取り、最後の点が値の到着直後に強く脈打ち、残る振動だけを残して値が書き換わります。

スパークラインの流れ
新しいタブで開く

07 · スパークラインの流れ — 計測 累積面積 11.706% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

easeOutCubicrequestAnimationFramepath

計算の見どころ

動いたフレーム23/24・累積面積11.706パーセントを記録しました。一覧の行やカードの中のミニ指標に置きます — カードの枠も線も数字までも全部 canvas が描き、広いときは2+3の二段、狭いときは縦一列に自ら配置を変えます。

// カード i のループ時刻 p(0..1) の状態 — 何回押されたか(shift)と今の押しの進行(slide 0..1、イージング適用)
function swapState(i, p) {
  var shift = 0;
  if (p >= SWAP_AT[1]) shift = 2;
  else if (p >= SWAP_AT[0]) shift = 1;
  if (shift === 0) return { shift: shift, slide: 0 };
  var lastAt = SWAP_AT[shift - 1] + i * STAG;
  var raw = Math.min(1, Math.max(0, (p - lastAt) / SWAP_DUR));
  return { shift: shift, slide: easeOutCubic(raw) };
}

時間窓は直近12個の値で、一度押すのに全体の10パーセント(SWAP_DUR)を使い、カードごとに1.8パーセント(STAG)の時間差を置いて互いにずれるようにしました。

08レーダーチャートの変形 — 頂点ごとに違う時計

画面に見えるもの

速度・正確度・安定性・探索・協働・持続の六軸の多角形がチームA(88, 82, 92, 78, 90, 84)からチームB(34, 26, 40, 22, 30, 28)へ頂点ごとに違う出発時差と速度倍率でモーフィングし、半透明の二枚が重なって共有する力が濃く見えます。

レーダーチャートの変形
新しいタブで開く

08 · レーダーチャートの変形 — 計測 累積面積 7.3% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

polygoneaseOutCubicglobalAlpha

計算の見どころ

動いたフレーム23/24・累積面積7.3パーセント — 九つの中で最も静かな動きです。能力の比較や二候補の対照に置きます — モーフィングが終わる62パーセント地点のあと、0.75〜0.90の区間で同じ倍率のままAへ戻ります。

var morphing = A_VAL.slice();
var back = Math.max(0, (p - 0.75) / (BACK1 - 0.75));   // 0.75〜0.90 の復帰
for (var m = 0; m < N; m++) {
  var fwd = easeOutCubic(vertexP(m, p, MORPH1 - MORPH0, MORPH0));
  var v = A_VAL[m] + (B_VAL[m] - A_VAL[m]) * fwd;
  // 復帰 — B に届いた後また A へ。復帰も頂点の倍率をそのまま使う
  var bk = easeOutCubic(Math.max(0, (back - VTX[m].off * 0.5) * VTX[m].rate));
  morphing[m] = v + (A_VAL[m] - v) * Math.min(1, bk);
}

六頂点の時差は0.00〜0.06、速度倍率は0.70〜1.35です。一括のトゥイーンでは多角形が塊で動いて比較対象という印象を与えられず、頂点がずれると二つのチームの違いが先に現れます。

09ヒートマップの波 — 対角線に広がる56セル

画面に見えるもの

月〜日の7日×09〜16時の8時間帯の56セルが左上から対角線の波のタイミングでひとつずつ満ち、色は値0〜100を20点間隔の五段階に写し、セルにマウスを載せると曜日・時間帯・値が canvas 内の白いカードに浮かびます。

ヒートマップの波
新しいタブで開く

09 · ヒートマップの波 — 計測 累積面積 34.969% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scalergbapointermove

計算の見どころ

動いたフレーム21/24・累積面積34.969パーセント — 九つの中で最も広く生きています。訪問時間帯や利用パターンの探索に置きます — 平均と最高のラベルは全部満ちたあとの80パーセント地点に浮かび、最高のセルは日曜13時の92です。

// 値(0〜100) → 五段階の濃度ランプのインデックス(0..4)。20点間隔の計量(scale)
function rampAt(v) {
  return Math.min(4, Math.floor(v / 20));
}

// セル(i,j) の満ちる進行(0..1) — 対角線の波: (i+j)*係数 + サイン位相
function cellAt(i, j, p) {
  var phase = Math.sin((i * 3.7 + j * 2.9)) * WAVE;   // セルごとに決まった位相のずれ(シード)
  var start = (i + j) * DIAG + phase;
  return Math.max(0, Math.min(1, (p - start) / GROW));
}

対角線係数 DIAG 0.045で同じ対角線どうしを並走させ、サイン位相の幅 WAVE 0.06が同じ対角線の中でもずらして順次と並行が混じった波を作ります — セルひとつが満ちる区間は10パーセント(GROW)です。

javascript グラフ 自作は何から決めるべきか?

軸のスケールを最初に決める

javascript グラフ作成は canvas 一枚とスクリプト一つで始められますが、座標の計算の前に決める数が二つあります。y軸の下限と範囲です — この記事では01が100〜500、05が50〜500、04は y 0〜500に x 0〜20(万ウォン)と、それぞれ決め打ちにしました。この数字をデータの最小値・最大値のままにすると余白は減りますが目盛りの数字が半端になり、丸く切れた数に置くと絵が安定します。棒(02)は目盛りを100単位の四本に固定して、データが変わっても軸が揺れないようにしています。

イージング関数が絵の性格を決める

同じデータでもイージングが違えば別のグラフになります。九つのうち五つ(02・05・07・08・09)が 1 - (1-t)^3 の減速イージングひとつを共有するのは、始まりが速く目標へ滑り込んで止まるこの曲線が「値が積もって落ち着く」図の性質と合うからです。一方06の針は減衰サイン波、01の端点は easeOutBack と、それぞれ別の関数を使いました。javascript グラフ ライブラリをこれから組む人にとって、イージングをどこに置くかは化粧ではなく、その絵が何を語るかの問題です。

devicePixelRatio で物理ピクセルを合わせる

九つ全部、ウィンドウの大きさが変わると fit() をやり直し、その中で cv.width = Math.round(W * dpr) に続けて ctx.setTransform(dpr, 0, 0, dpr, 0, 0) で座標系を合わせます(Canvas API の基本 — MDN)。この一行が抜けると2倍密度の画面で軸ラベルと値の数字が水を含んだように滲みます — グラフィックのコードは常に論理ピクセルで書き、物理解像度には最初の一度だけ触る構造です。

どこで壊れるか — 落とし穴

一番よく踏んだのは canvas の y 軸が下に開いている という事実です。データの値をそのまま y に載せると、売上が増えるほど線が下がるグラフになります — 01の yAt が 1 - (v - 下限) / 範囲 で反転している理由で、06が上側の半円をPI(左) → 1.5PI(頂点) → 2PI(右)の順で数えるのも同じ座標系の結果です。同じ反転がここでも働きます — canvas の角度は標準の単位円とは逆に時計回りへ増えるので、03 が-90度から始めて角度を増やしていくとスライスが順番どおりに並ぶのです。二つ目はトラック(薄く敷いておく居場所)です — 03で最初のフレームにトラック全体を敷くと、その場所は最後まで変わらず画面の半分が死んでいます。ペンが通った分だけトラックを開く今の構造に変えてから、累積面積が生き返りました。

何が壊れるか どう現れるか どう対処したか
y 座標に値をそのまま載せる 値が増えるほど下がるグラフ 1-(v-下限)/範囲 で反転
dpr 未設定 2倍画面でラベル・数字が滲む setTransform(dpr,0,0,dpr,0,0)
最初のフレームでトラック全面を敷く その場所が最後まで変わらない 描いた分だけトラックを開く (03)
roundRect 非対応のブラウザ エラーで canvas 全体が空になる typeof 検査の後 rect へのフォールバック (04)
チップ色の上の値ラベル コントラストが基準に届かない 白い舞台の上に置く (09)

九つをひとまとめにして受け取る ZIP の開封パスワードは 6ypv8dkk で、展開すると各項目にブラウザでそのまま開ける HTML とスタイルがひと組ずつ入っています。

アクセシビリティ

canvas は絵なので、スクリーンリーダーには空の箱です — だから九つ全部に role="img" と何が描かれているかを書いた aria-label を付け(MDN aria-label)、隣には画面から隠した sr-only の表で同じデータを文字で提供します(04は40行では長いので代表8個、09は時間帯を二つずつまとめた要約)。色だけで意味を伝えない WCAG 1.4.1 の原則に従い、03のドーナツは凡例に名前とパーセントを、09のヒートマップは平均と最高のセルを文字で重ねます。

prefers-reduced-motion: reduce では九つ全部がループを外して完成した一枚だけを描きます — 01は12週全部が引かれて端点が膨らんだ画面、02は伸び切った棒とカウントアップの終わった値、03はリングが満ちて合計24,000が浮かんだ画面です。04と09は静止画でも mousemove が来たときだけ描き直して応答するので、動きを減らした人にもハイライトとツールチップは生きています。06は針が72で確定した画面(0.95)、08は二つの多角形が重なったチームB到達の画面(0.75)でそれぞれ止まります。

FAQ

グラフ作成サイトのように開いてすぐ試せる画面としても使えますか?

使えます。各項目の HTML はファイルを開けばそのまま動く一枚の文書なので、受け取ったファイルをブラウザで開くだけで値の入力なしにデモ画面が動きます。この記事のデモ窓も同じファイルをそのまま埋め込んだものなので、ページで確かめてファイルから持ち帰る絵は同じです。

javascript グラフ ライブラリを使うのと何が違いますか?

ライブラリは軸・凡例・ツールチップを一度に得る代わりに、描き方をライブラリの流儀に預けます。この九つは外部依存がゼロなのでファイルを一枚コピーすればどんなページにも乗り、目盛りが何本か(02は100単位の四本)、スライスの隙間がどれだけか(03は0.035ラジアン)、針が何回揺れるか(06は減衰5.2・角振動数10.5)を全部コードで直接決めます。ダッシュボードが大きくなって交叉するツールチップの同期が要るならそこからがライブラリの出番で、自分のものにしたい一つ二つのグラフにはこちらが短いです。

実際のデータに差し替えるにはどこを書き換えればよいですか?

各ファイルの先頭にあるデータ配列がすべてです — 01の WEEKS、02の ROWS、03の SRC、09の GRID のように値を書き並べた配列をあなたのデータに変えると、座標計算・アニメーション・ラベルが一緒に付いてきます。ただし y 軸の下限と範囲(01は Y_TOP 100・Y_SPAN 500)はデータに合わせて手を入れないと絵が歪みます。その基準は上の「何から決めるべきか」の節にまとめてあります。

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

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