Chart.js の内部構造:アーキテクチャ概要とコードベースツアー
前提知識
- ›JavaScript/TypeScript の基本的な知識
- ›軸・データセット・シリーズなど、チャートの基本概念への理解
- ›npm/pnpm パッケージ管理ツールの使用経験
Chart.js の内部構造:アーキテクチャ概要とコードベースツアー
Chart.js はウェブで最も広く使われているチャートライブラリのひとつです。しかし多くの開発者は、設定 API にオブジェクトやコールバックを渡すだけで、その裏側で何が起きているかをほとんど意識したことがないのではないでしょうか。このシリーズでは、そこに踏み込んでいきます。全 6 回にわたり、モジュールの構成から Canvas の fillRect 呼び出しに至るまで、コードベースのあらゆる経路を追いかけます。第 1 回の本稿では全体像を把握することを目標に、ソースコードの構成・ビルドパイプラインの仕組み・そして用途に応じた 3 つのエントリーポイントの設計思想を丁寧に見ていきます。
プロジェクト構造とモジュール構成
src/ ディレクトリには 7 つのトップレベルディレクトリがあり、それぞれが独立したアーキテクチャ層を担っています。
| ディレクトリ | 役割 | 主なエクスポート |
|---|---|---|
core/ |
オーケストレーション、ライフサイクル、設定、レイアウト | Chart, Scale, DatasetController, defaults, registry |
controllers/ |
チャートタイプのロジック(bar、line、doughnut など) | BarController, LineController, DoughnutController, … |
elements/ |
Canvas に描画される視覚的プリミティブ | ArcElement, BarElement, LineElement, PointElement |
scales/ |
軸の種類と座標マッピング | LinearScale, LogarithmicScale, TimeScale, CategoryScale |
plugins/ |
横断的な機能(凡例、ツールチップ、カラーなど) | Legend, Tooltip, Filler, Decimation, Colors |
platform/ |
実行環境の抽象化(DOM、OffscreenCanvas) | DomPlatform, BasicPlatform, BasePlatform |
helpers/ |
共通ユーティリティ関数群 | 数学計算、色処理、DOM 操作、イージング、設定解決 |
これらのディレクトリは単なる整理用のフォルダではありません。依存の流れが一方向になるよう意図的に設計されたレイヤー構造を反映しています。controllers は core と elements に依存し、plugins は core に依存し、helpers は他に依存しないリーフノードとして機能します。
graph TD
subgraph "Public API"
CHART["Chart (core.controller)"]
end
subgraph "Subsystems"
CTRL["Controllers"]
SCALE["Scales"]
PLUG["Plugins"]
ELEM["Elements"]
PLAT["Platform"]
end
subgraph "Foundation"
CORE["Core (defaults, registry, layouts, config)"]
HELP["Helpers"]
end
CHART --> CTRL
CHART --> SCALE
CHART --> PLUG
CHART --> PLAT
CHART --> CORE
CTRL --> ELEM
CTRL --> CORE
SCALE --> CORE
PLUG --> CORE
CORE --> HELP
ELEM --> HELP
PLAT --> HELP
core のバレルファイルは、アニメーター・レジストリ・デフォルト設定・レイアウトエンジンのシングルトンインスタンスを含め、ライブラリ全体が必要とするものをすべて再エクスポートしています。
ここではクラスとシングルトンインスタンスの両方がエクスポートされている点に注目してください。Chart クラスは core.controller.js のデフォルトエクスポートですが、defaults、registry、layouts、animator はあらかじめインスタンス化されたシングルトンです。この違いは tree-shaking を考えるうえで非常に重要な意味を持ちます。詳しくは後ほど説明します。
エントリーポイントと exports マップ
Chart.js は package.json の exports マップを通じて 3 つの公開エントリーポイントを提供しています。
flowchart LR
subgraph "Consumer Code"
A["import { Chart } from 'chart.js'"]
B["import 'chart.js/auto'"]
C["import { color } from 'chart.js/helpers'"]
end
subgraph "Entry Points"
ESM["src/index.ts<br/>(tree-shakeable)"]
AUTO["auto/auto.js<br/>(side-effectful)"]
HELP["src/helpers/index.ts<br/>(utilities)"]
end
A --> ESM
B --> AUTO
C --> HELP
AUTO -->|"imports & registers"| ESM
ESM エントリー(src/index.ts)は各サブシステムのすべてを再エクスポートし、registerables 配列も提供しています。重要なのは、ここでは Chart.register() を呼び出さないという点です。これにより、バンドラーが実際に使われているインポートだけを静的解析で特定し、不要なコードを除去できます。
auto エントリー(auto/auto.js)は dist の出力から Chart と registerables をインポートし、即座に Chart.register(...registerables) を呼び出します。これは package.json で sideEffects としてマークされており、戻り値が使われていなくてもバンドラーがこのインポートを除去できないことを示しています。
UMD エントリー(src/index.umd.ts)はさらに踏み込んでいます。すべてのコンポーネントを自動登録し、Object.assign で Chart 名前空間にまとめ、window.Chart としてグローバルに公開します。CDN で配信される「フル装備」のバンドルはこれにあたります。
ヒント: モダンなアプリケーションで bar と line チャートだけが必要な場合は、
chart.js/autoではなくchart.jsからインポートし、必要なコンポーネントだけを登録しましょう。バンドルサイズを 30〜40% 削減できます。
ビルドパイプライン:ソースから配布物へ
Chart.js は Rollup と SWC を組み合わせて TypeScript をトランスパイルし、1 つの設定ファイルから 4 種類の出力バンドルを生成します。
flowchart TD
SRC_UMD["src/index.umd.ts"] --> ROLLUP_UMD["Rollup + SWC + Terser"]
SRC_ESM["src/index.ts"] --> ROLLUP_ESM["Rollup + SWC + Cleanup"]
SRC_HELP["src/helpers/index.ts"] --> ROLLUP_ESM
ROLLUP_UMD --> UMD_MIN["dist/chart.umd.min.js<br/>(UMD, minified)"]
ROLLUP_UMD --> UMD["dist/chart.umd.js<br/>(UMD, minified)"]
ROLLUP_ESM --> ESM_OUT["dist/chart.js<br/>(ESM)"]
ROLLUP_ESM --> CJS_OUT["dist/chart.cjs<br/>(CommonJS)"]
ROLLUP_ESM --> HELP_OUT["dist/helpers.js<br/>(ESM)"]
ビルドパイプラインのプラグイン設定には、見落としがちながら重要な点があります。
UMD ビルドでは Terser がミニファイを担当しますが、ESM/CJS ビルドでは代わりに rollup-plugin-cleanup が使われ、comments: ['some', /__PURE__/] という設定が適用されています。これにより出力に #__PURE__ アノテーションが保持され、下流のバンドラーが副作用のない式を識別できるようになります。
SWC の設定ターゲットは es2022 です。つまり出力はクラスフィールドやオプショナルチェーニングといったモダンな構文を使います。これは意図的な選択であり、古いブラウザをサポートしたい場合は利用者側でさらにトランスパイルすることを前提とした設計です。
シングルトンパターンと Tree-Shaking
Chart.js は Defaults ストア、Registry、Animator という 3 つの重要なシングルトンに依存しています。それぞれモジュールスコープで /* #__PURE__ */ アノテーション付きでインスタンス化されます。
src/core/core.defaults.js#L165-L175
src/core/core.registry.js#L185-L186
src/core/core.animator.js#L213-L214
classDiagram
class Defaults {
+backgroundColor: string
+borderColor: string
+color: string
+font: object
+set(scope, values)
+get(scope)
+route(scope, name, targetScope, targetName)
+describe(scope, values)
+override(scope, values)
}
class Registry {
+controllers: TypedRegistry
+elements: TypedRegistry
+plugins: TypedRegistry
+scales: TypedRegistry
+add(...args)
+remove(...args)
}
class Animator {
-_charts: Map
-_running: boolean
+listen(chart, event, cb)
+add(chart, items)
+start(chart)
+stop(chart)
}
Defaults <.. Registry : uses for merging
Animator <.. Chart : drives render loop
Registry <.. Chart : resolves components
#__PURE__ アノテーションは webpack や Rollup のようなバンドラーへの指示です。「この式に副作用はない。結果が誰にもインポートされなければ、安全に削除してよい」という意味を持ちます。このアノテーションがなければ、バンドラーは new Defaults(...) がグローバルな状態を変更する可能性があると判断し、削除を諦めなければなりません。
ESM エントリーが tree-shaking に対応できているのはこの仕組みのおかげです。Chart と BarController だけをインポートすれば、バンドラーは PolarAreaController や RadarController、それらに紐づく要素が一切参照されていないと判断し、除去することができます。
モジュール依存グラフ
ESM エントリーポイント(src/index.ts)は、スター形式のエクスポートパターンを使ってサブシステムのバレルファイルからライブラリ全体を構成しています。また各サブシステムを名前空間化し、registerables 配列としてエクスポートしています。
graph TD
INDEX["src/index.ts"]
INDEX -->|"export *"| CTRL_IDX["controllers/index.js"]
INDEX -->|"export *"| CORE_IDX["core/index.ts"]
INDEX -->|"export *"| ELEM_IDX["elements/index.js"]
INDEX -->|"export *"| PLAT_IDX["platform/index.js"]
INDEX -->|"export *"| PLUG_IDX["plugins/index.js"]
INDEX -->|"export *"| SCALE_IDX["scales/index.js"]
INDEX -->|"import * as controllers"| CTRL_IDX
INDEX -->|"import * as elements"| ELEM_IDX
INDEX -->|"import * as plugins"| PLUG_IDX
INDEX -->|"import * as scales"| SCALE_IDX
subgraph "registerables array"
REG["[controllers, elements, plugins, scales]"]
end
INDEX --> REG
UMD エントリーはまったく異なるアプローチを取っています。再エクスポートではなく、すべてをインポートしたうえで Chart オブジェクトに手動で紐づけます。
Object.assign(Chart, controllers, scales, elements, plugins, platforms) の呼び出しによって、すべてのエクスポートが単一の Chart 名前空間にまとめられます。これは ESM 以前の利用者が期待する Chart.LineController、Chart.LinearScale というアクセスパターンに対応するためです。そして 50 行目での window.Chart の設定が、<script> タグによるグローバル登録を完成させます。
ここに込められた設計の核心は、同じソースコードが 3 つの利用パターン——tree-shaken ESM、自動登録 ESM、グローバル UMD——をロジックの重複なく支えているという点です。エントリーポイントがサブシステムをどのように組み合わせて公開するか、その違いだけで実現されています。
次回へのつながり
コードベースの全体像を把握したところで、次はその中を走る最も重要な経路を追いかけましょう。開発者が new Chart(ctx, config) と書いたとき、内部では何が起きているのでしょうか。次回の記事では、Chart コンストラクターを出発点に、プラットフォーム検出・設定の解決・多段階の update() パイプライン・そしてデータを Canvas 上のピクセルへと変換するレイヤー構造の draw() システムまでを丁寧に追っていきます。