Source

140 chart lines · 3 files · 4.2 kBBenchmark harness excluded · shared/mount.ts

Flare package hierarchy252 records · CSV · 8.0 kB
@charts-poc/demo-data/flare

Selection: Complete published snapshot

name
string
size
number | null

Flare visualization toolkit@observablehq/sample-datasets@1.0.1 · revision 732c0148de74 · flare.csv · ISC distribution; upstream source credited · SHA-256 704349b51b70Pinned snapshot

cases/101-sunburst/tanstack.ts92 lines · entry
cases/101-sunburst/tanstack.ts
import { defineChart } from '@tanstack/charts'
import { polar, radialArc } from '@tanstack/charts/polar'
import { flare } from '@charts-poc/demo-data/flare'
import { partition } from 'd3-hierarchy'
import { arc } from 'd3-shape'
import { selectSunburstData } from './selection'
import { flareHierarchy } from './transform'
import { tanstackMount } from '../../shared/mount'
import type { FlareRow } from '@charts-poc/demo-data/flare'
import type { ConformanceInput } from '../../types'
import type { HierarchyRectangularNode } from 'd3-hierarchy'

interface SunburstArcDatum {
  name: string
  value: number
  depth: number
  startAngle: number
  endAngle: number
  category: string
}

function topLevelCategory(node: HierarchyRectangularNode<FlareRow>): string {
  return (
    node.ancestors().find((ancestor) => ancestor.depth === 1)?.data.name ??
    'Other'
  )
}

const definition = (input: ConformanceInput) => {
  const root = flareHierarchy(selectSunburstData(flare, input.revision)).sum(
    (node) => node.size ?? 0,
  )
  const layout = partition<FlareRow>().size([Math.PI * 2, root.height + 1])(
    root,
  )
  const data: SunburstArcDatum[] = layout
    .descendants()
    .filter((node) => node.depth > 0)
    .map((node) => ({
      name: node.data.name,
      value: node.value ?? 0,
      depth: node.depth,
      startAngle: Math.PI / 2 - node.x0,
      endAngle: Math.PI / 2 - node.x1,
      category: topLevelCategory(node),
    }))

  return defineChart({
    marks: [
      polar({
        radiusRatio: 0.88,
        marks: [
          radialArc(data, {
            className: 'ts-chart__sunburst',
            generator: ({ radius }) => {
              const innerRadius = radius * 0.14
              const treeDepth = root.height + 1
              const thickness = (radius - innerRadius) / treeDepth
              const ringPadding = 2

              return arc<unknown, SunburstArcDatum>()
                .startAngle((node) => node.startAngle)
                .endAngle((node) => node.endAngle)
                .innerRadius(
                  (node) =>
                    innerRadius + (node.depth - 1) * (thickness + ringPadding),
                )
                .outerRadius(
                  (node) =>
                    innerRadius +
                    (node.depth - 1) * (thickness + ringPadding) +
                    thickness,
                )
            },
            color: 'category',
            stroke: '#ffffff',
            strokeWidth: 2,
          }),
        ],
      }),
    ],
    color: {
      range: ['#7c3aed', '#0ea5e9', '#14b8a6'],
    },
    margin: 0,
  })
}

export const mount = tanstackMount(definition, 'Flare analytics sunburst', {
  format: ({ datum }) =>
    `${datum.name.replaceAll('.', ' › ')} · ${datum.value.toLocaleString('en-US')}`,
})
cases/101-sunburst/selection.ts7 lines · support
cases/101-sunburst/selection.ts
import type { FlareRow } from '@charts-poc/demo-data/flare'

export function selectSunburstData(rows: readonly FlareRow[], revision = 0) {
  return revision % 2 === 0
    ? rows.slice(1, 12)
    : [...rows.slice(1, 8), ...rows.slice(9, 13)]
}
cases/101-sunburst/transform.ts41 lines · support
cases/101-sunburst/transform.ts
import { stratify } from 'd3-hierarchy'
import type { FlareRow } from '@charts-poc/demo-data/flare'
import type { HierarchyNode } from 'd3-hierarchy'

export interface SunburstTreeNode {
  [key: string]: unknown
  name: string
  size: number | null
  value: number
  children?: SunburstTreeNode[]
}

export function flareHierarchy(rows: readonly FlareRow[]) {
  const rootName = rows[0]?.name
  if (rootName === undefined) {
    throw new TypeError('The Flare sunburst selection is empty')
  }

  return stratify<FlareRow>().path((row) =>
    row.name.slice(rootName.length).replaceAll('.', '/'),
  )(Array.from(rows))
}

export function sunburstTree(rows: readonly FlareRow[]): SunburstTreeNode {
  return treeNode(flareHierarchy(rows))
}

function treeNode(node: HierarchyNode<FlareRow>): SunburstTreeNode {
  const children = node.children?.map(treeNode)
  const value =
    children === undefined
      ? (node.data.size ?? 0)
      : children.reduce((total, child) => total + child.value, 0)

  return {
    name: node.data.name,
    size: node.data.size,
    value,
    ...(children === undefined ? {} : { children }),
  }
}