← ./articles-ja

D3は1つのhierarchyからtreemapとsunburstを描き分ける

Treemapとsunburstは、同じ階層データを違う形で見せることが多いです。

ここで、矩形用と円弧用に別々のdata pipelineを作ると、filter、sort、集計、drill-downが少しずつズレます。最初は動いても、片方だけ修正されて、2つのviewが違う答えを返すようになります。

階層構築は1回の共通関数にまとめ、layoutだけを分けます。

症状: 2つのchartが食い違う

よくある症状です。

  • treemapとsunburstの合計値が違う
  • filterが片方にしか効かない
  • breadcrumbが一致しない
  • export結果のnode数が違う
  • bug fixを2箇所に入れる必要がある

同じtreeを表すなら、tree constructionは共有すべきです。

hierarchy構築とlayoutを分ける

viewに依存しないbuilderを作ります。

import { hierarchy, type HierarchyNode } from "d3-hierarchy";

type TreeItem = {
  name: string;
  value?: number;
  children?: TreeItem[];
};

export function buildHierarchy(data: TreeItem[]): HierarchyNode<TreeItem> {
  return hierarchy({ name: "root", children: data })
    .sum((node) => node.value ?? 0)
    .sort((a, b) => (b.value ?? 0) - (a.value ?? 0));
}

この関数には、filter、grouping、label、value計算を入れます。treemapの座標やsunburstの角度は入れません。

layoutはviewごとに適用する

Treemap側です。

import { treemap } from "d3-hierarchy";

export function buildTreemap(data: TreeItem[], width: number, height: number) {
  const root = buildHierarchy(data);
  return treemap<TreeItem>()
    .size([width, height])
    .paddingInner(2)(root);
}

Sunburst側です。

import { partition } from "d3-hierarchy";

export function buildSunburst(data: TreeItem[], radius: number) {
  const root = buildHierarchy(data);
  return partition<TreeItem>()
    .size([2 * Math.PI, radius])(root);
}

D3 layoutはnodeに座標情報をmutateします。だから同じroot objectを共有せず、viewごとにrootを作ります。

drill-downはpathで保持する

D3 node objectをそのままずっと保持すると、filterやdata更新後にstaleになります。

安定したpathを持ちます。

type DrillState = {
  path: string[];
};

再描画時に、新しいhierarchyから探し直します。

function findByPath(root: HierarchyNode<TreeItem>, path: string[]) {
  let current: HierarchyNode<TreeItem> | undefined = root;

  for (const name of path) {
    current = current.children?.find((child) => child.data.name === name);
    if (!current) return null;
  }

  return current;
}

これで、filter変更後もbreadcrumbとdrill targetが一致します。

exportではCSS変数を解決する

SVGが var(--color-section) のようなCSS変数を使っている場合、アプリ外で開いたSVGの色が失われることがあります。

export前に次を行います。

  1. SVGをcloneする
  2. descendantを走査する
  3. getComputedStyle() を読む
  4. fillstrokecolor をinline化する
  5. widthheightxmlns を明示する
  6. XMLSerializer でserializeする

可視化ツールでは、export品質も製品価値の一部です。

検証チェック

  • treemapとsunburstの合計値が一致する
  • 同じfilterが両方に効く
  • filter後のnode数が一致する
  • data更新後もbreadcrumbが壊れない
  • SVG exportの色がアプリ外でも残る
  • PNG exportがドキュメント貼り付けに耐える解像度になる

参考