UNPKG

@tanstack/charts

Version:

A chart grammar for TypeScript and JavaScript. Marks consume your data directly, channels describe visual encodings, and the engine compiles them into a renderer-neutral keyed scene. TanStack's compact scales cover common numeric and categorical mappings.

291 lines (290 loc) 10 kB
import { treemap as createTreemapLayout, treemapBinary, treemapDice, treemapSlice, treemapSliceDice, treemapSquarify } from "d3-hierarchy"; import { measureSceneLabelBounds } from "./guide-layout.js"; import { aggregateFlatHierarchyValues, buildFlatHierarchy, flatHierarchyAncestorIds, flatHierarchyNodeContext, flatHierarchyNodeValue } from "./hierarchy-flat-internal.js"; import { channelValues, isChartKey, markStates, visualValue } from "./mark.js"; import { createMarkWithScaleValues } from "./mark-with-scale-values.js"; import { valueKey } from "./scales.js"; function treemap(source, options) { const hierarchyOptions = options.path !== void 0 ? { path: options.path, delimiter: options.delimiter } : { id: options.nodeId, parentId: options.parentId }; const hierarchy = buildFlatHierarchy(source, hierarchyOptions, "treemap"); aggregateFlatHierarchyValues(hierarchy, options.value, "treemap"); const contexts = /* @__PURE__ */ new WeakMap(); const context = (node) => { const existing = contexts.get(node); if (existing) return existing; const created = Object.freeze(treemapNodeContext(node)); contexts.set(node, created); return created; }; if (options.sort) { hierarchy.root.sort((left, right) => { const compared = options.sort( context(left), context(right) ); if (!Number.isFinite(compared)) { throw new TypeError("treemap: sort result must be finite"); } return compared; }); } const method = options.method ?? "squarify"; const ratio = options.ratio ?? (1 + Math.sqrt(5)) / 2; assertMethod(method); if (options.ratio !== void 0 && method !== "squarify") { throw new TypeError('treemap: ratio is only valid with method "squarify"'); } if (!Number.isFinite(ratio) || ratio < 1) { throw new TypeError("treemap: ratio must be finite and at least 1"); } const paddingInner = options.paddingInner ?? 0; const paddingOuter = options.paddingOuter ?? 0; assertNonnegativeFinite(paddingInner, "paddingInner"); assertNonnegativeFinite(paddingOuter, "paddingOuter"); const inset = options.inset ?? 0.75; const labelPadding = options.labelPadding ?? 4; assertNonnegativeFinite(inset, "inset"); assertNonnegativeFinite(labelPadding, "labelPadding"); return createMarkWithScaleValues(({ markIndex }) => { const id = options.id ?? `treemap-${markIndex}`; return { id, channels: {}, seriesFromColor: options.color !== void 0, resolveLayout: ({ chart, layout }) => { const root = hierarchy.root.copy(); const laidOut = configureLayout( createTreemapLayout(), chart.width, chart.height, method, ratio, options.round ?? false, paddingInner, paddingOuter )(root); const leaves = laidOut.leaves(); assertLayoutCoordinates(leaves, chart.width, chart.height); const cells = leaves.map((node) => materializeCell(node, chart.x, chart.y)).filter((cell) => cell.x1 > cell.x0 && cell.y1 > cell.y0); const nodes = cells.map((cell) => cell.node); const colorValues = channelValues(nodes, options.color, () => null); const labels = materializeLabels( cells, options.label, options.labelFontSize ?? 11, options.labelFontWeight, inset, labelPadding, layout.measureText, id ); return { channels: { color: { scale: "color", values: colorValues.filter(isChartKey) } }, states: markStates(nodes, options.states), render: ({ color: resolveColor, theme }) => { const points = []; const children = []; cells.forEach((cell, nodeIndex) => { const node = cell.node; const colorValue = colorValues[nodeIndex]; const fallback = resolveColor( isChartKey(colorValue) ? colorValue : null ); const fill = visualValue( options.fill, node, nodeIndex, nodes, fallback ); const key = `${id}:node:${valueKey(node.id)}`; const group = isChartKey(colorValue) ? colorValue : null; const point = { key, markId: id, group, groupLabel: group === null ? id : String(group), datum: node, datumIndex: nodeIndex, xValue: node.id, yValue: node.value, x: cell.x, y: cell.y, color: fill }; points.push(point); children.push({ kind: "rect", key, x: cell.x0 + inset, y: cell.y0 + inset, width: Math.max(0, cell.x1 - cell.x0 - inset * 2), height: Math.max(0, cell.y1 - cell.y0 - inset * 2), radius: options.radius, inset, insetAxis: "xy", interaction: { point }, style: { fill, fillOpacity: options.fillOpacity, stroke: options.stroke === void 0 ? void 0 : visualValue( options.stroke, node, nodeIndex, nodes, fallback ), strokeOpacity: options.strokeOpacity, strokeWidth: options.strokeWidth } }); const label = labels.get(node.id); if (label) { children.push({ ...label, key: `${key}:label`, pointOwner: point, style: { fill: visualValue( options.labelFill, node, nodeIndex, nodes, theme.foreground ) } }); } }); return { nodes: [ { kind: "group", key: id, className: "ts-chart__treemap ts-chart__rect ts-chart__text", ariaHidden: true, children } ], points }; } }; } }; }, options.motion); } function configureLayout(layout, width, height, method, ratio, round, paddingInner, paddingOuter) { const tile = typeof method === "function" ? method : method === "squarify" ? treemapSquarify.ratio(ratio) : method === "binary" ? treemapBinary : method === "dice" ? treemapDice : method === "slice" ? treemapSlice : treemapSliceDice; return layout.size([width, height]).tile(tile).round(round).paddingInner(paddingInner).paddingOuter(paddingOuter); } function treemapNodeContext(node) { return { ...flatHierarchyNodeContext(node), ancestorIds: flatHierarchyAncestorIds(node), value: flatHierarchyNodeValue(node) }; } function materializeCell(node, offsetX, offsetY) { const x0 = offsetX + node.x0; const y0 = offsetY + node.y0; const x1 = offsetX + node.x1; const y1 = offsetY + node.y1; return { node: treemapNodeContext(node), x0, y0, x1, y1, x: (x0 + x1) / 2, y: (y0 + y1) / 2 }; } function assertLayoutCoordinates(nodes, width, height) { nodes.forEach((node) => { const coordinates = [node.x0, node.y0, node.x1, node.y1]; if (!coordinates.every(Number.isFinite)) { throw new TypeError( `treemap: layout produced non-finite coordinates for node "${node.data.id}"` ); } if (node.x1 < node.x0 || node.y1 < node.y0) { throw new TypeError( `treemap: layout produced reversed coordinates for node "${node.data.id}"` ); } if (node.x0 < 0 || node.y0 < 0 || node.x1 > width || node.y1 > height) { throw new TypeError( `treemap: layout produced out-of-bounds coordinates for node "${node.data.id}"` ); } }); } function materializeLabels(cells, channel, fontSize, fontWeight, inset, padding, measureText, id) { if (channel === void 0) return /* @__PURE__ */ new Map(); const nodes = cells.map((cell) => cell.node); const values = channelValues(nodes, channel, () => null); const labels = /* @__PURE__ */ new Map(); cells.forEach((cell, index) => { const node = cell.node; const value = values[index]; if (value == null || String(value).length === 0) return; const label = { kind: "label", key: `${id}:label:${valueKey(node.id)}`, x: cell.x, y: cell.y, text: String(value), anchor: "middle", baseline: "middle", fontSize, fontWeight }; const bounds = measureSceneLabelBounds(label, measureText); const left = cell.x0 + inset + padding; const right = cell.x1 - inset - padding; const top = cell.y0 + inset + padding; const bottom = cell.y1 - inset - padding; if (bounds.x >= left && bounds.x + bounds.width <= right && bounds.y >= top && bounds.y + bounds.height <= bottom) { labels.set(node.id, label); } }); return labels; } function assertMethod(value) { if (typeof value === "function") return; if (value !== "squarify" && value !== "binary" && value !== "dice" && value !== "slice" && value !== "slice-dice") { throw new TypeError(`treemap: invalid method "${value}"`); } } function assertNonnegativeFinite(value, description) { if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { throw new TypeError( `treemap: ${description} must be nonnegative and finite` ); } } export { treemap };