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.

284 lines (283 loc) 9.41 kB
import { valueKey } from "./scales.js"; import { createScenePointLookup, sceneNodeOwnedPoints } from "./scene-point-ownership-internal.js"; const emptyPoints = []; function resolveFocusPresentation(scene, focus, pointer, cursor) { const focusedScene = resolveFocusScene(scene, focus).scene; const focusedUnder = focusedSceneNodes(focusedScene, focus, "under"); const focusedOver = focusedSceneNodes(focusedScene, focus, "over"); const guides = resolveFocusGuides(scene, focus, pointer, cursor); return { under: [...guides.under, ...focusedUnder], over: [...focusedOver, ...guides.over] }; } function resolveFocusGuides(scene, focus, pointer, cursor) { const under = []; const over = []; if (!cursor && !focus) return { under, over }; for (const guide of scene.focusGuides ?? []) { const localFocus = focus && guideOwnsFocus(guide, focus) ? focus : null; if (!cursor && focus && !localFocus) continue; const node = guide.resolve({ scene, guide, focus: localFocus, pointer, cursor }); if (!node) continue; (guide.placement === "under" ? under : over).push(node); } return { under, over }; } function resolveFocusScene(scene, focus) { if (!focus) return { scene, retargeted: false }; let retargeted = false; const visit = (nodes2) => nodes2.map((node) => { if (node.kind !== "group") return node; if (node.focus?.retarget) { const points = node.focus.points.filter( (point) => matchesFocusAnchor(point, focus, node.focus.match) ); const lookup = createScenePointLookup(node.focus.points); const selected = stabilizeSelectedNodes( filterNodesWithLookup( node.focus.candidates ?? node.children, points, node.focus.points, lookup ), points, node.focus.points, lookup, node.key ); if (!selected.length) return node; retargeted = true; return { ...node, focus: { ...node.focus, activePoints: points }, children: selected }; } const children = visit(node.children); return children.some((child, index) => child !== node.children[index]) ? { ...node, children } : node; }); const nodes = visit(scene.nodes); return retargeted ? { scene: { ...scene, nodes }, retargeted } : { scene, retargeted }; } function focusedSceneNodes(scene, focus, placement) { if (!focus) return []; return collectFocusedNodes(scene.nodes, focus, placement); } function focusedNodeKeys(layer, focus) { if (!layer.focus || !focus) return /* @__PURE__ */ new Set(); const keys = /* @__PURE__ */ new Set(); visitNodes(selectedFocusChildren(layer, focus), (node) => keys.add(node.key)); return keys; } function collectFocusedNodes(nodes, focus, placement) { const output = []; for (const node of nodes) { if (node.kind !== "group") continue; if (node.focus) { if (node.focus.placement !== placement) continue; const children2 = selectedFocusChildren(node, focus); if (children2.length) output.push({ ...node, focus: void 0, children: children2 }); continue; } const children = collectFocusedNodes(node.children, focus, placement); if (children.length) output.push({ ...node, children }); } return output; } function selectedFocusChildren(layer, focus) { const state = layer.focus; if (state.retarget) return layer.children; if (state.anchors) { const anchors = state.anchors.filter( (anchor) => matchesFocusAnchor(anchor, focus, state.match) ); return filterNodesByAnchors(layer.children, anchors); } const points = state.points.filter( (point) => matchesFocusAnchor(point, focus, state.match) ); return filterNodes(layer.children, points, state.points); } function filterNodes(nodes, selectedPoints, candidatePoints) { return filterNodesWithLookup( nodes, selectedPoints, candidatePoints, createScenePointLookup(candidatePoints) ); } function filterNodesByAnchors(nodes, anchors) { const output = []; for (const node of nodes) { if (node.kind !== "group") { if (anchors.some((anchor) => keysRelate(node.key, anchor.key))) { output.push(node); } continue; } const children = filterNodesByAnchors(node.children, anchors); if (children.length) { output.push({ ...node, children }); } else if (anchors.some((anchor) => anchor.key.startsWith(`${node.key}:`))) { output.push(node); } } return output; } function filterNodesWithLookup(nodes, selectedPoints, candidatePoints, lookup) { const output = []; for (const node of nodes) { if (node.kind !== "group") { if (sceneNodeOwnedPoints(node, candidatePoints, lookup, emptyPoints).some( (point) => selectedPoints.includes(point) )) { output.push(node); } continue; } const structuralPoint = focusCandidatePoint(node, candidatePoints); if (structuralPoint) { if (selectedPoints.includes(structuralPoint)) output.push(node); continue; } const atomicPoints = atomicGroupPoints(node, candidatePoints, lookup); if (atomicPoints.length) { if (atomicPoints.some((point) => selectedPoints.includes(point))) { output.push(node); } continue; } const structuralPoints = sceneNodeOwnedPoints( node, candidatePoints, lookup, emptyPoints ); const childPoints = structuralPoints.length ? structuralPoints : candidatePoints; const children = filterNodesWithLookup( node.children, selectedPoints, childPoints, lookup ); if (children.length) { output.push({ ...node, children }); } } return output; } function stabilizeSelectedNodes(nodes, points, candidatePoints, lookup, layerKey) { const slots = new Map(points.map((point, index) => [point, index])); const visit = (node, path) => { const related = sceneNodeOwnedPoints( node, candidatePoints, lookup, emptyPoints ).filter((point2) => slots.has(point2)); const point = related.length === 1 ? related[0] : void 0; let key = node.key; if (point && node.key !== point.markId) { const slot = `${layerKey}:selection:${slots.get(point) ?? 0}`; if (node.key === point.key) key = slot; else if (node.key.startsWith(`${point.key}:`)) { key = `${slot}${node.key.slice(point.key.length)}`; } else if (point.key.startsWith(`${node.key}:`)) { key = `${slot}:ancestor:${path}`; } else { key = `${slot}:node:${path}`; } } return node.kind === "group" ? { ...node, key, children: node.children.map( (child, index) => visit(child, `${path}:${index}`) ) } : { ...node, key }; }; return nodes.map((node, index) => visit(node, String(index))); } function atomicGroupPoints(node, candidatePoints, lookup) { const candidate = focusCandidatePoint(node, candidatePoints); if (candidate) return [candidate]; if (node.pointOwner) { const owned = sceneNodeOwnedPoints( node, candidatePoints, lookup, emptyPoints ); if (owned.length) return owned; } const exact = lookup.keys.get(node.key)?.filter((point) => point.key === node.key); return exact === void 0 ? emptyPoints : exact.filter((point) => candidatePoints.includes(point)); } function focusCandidatePoint(node, candidatePoints) { const index = node.focusCandidateIndex; if (index === void 0 || !Number.isInteger(index) || index < 0) { return void 0; } return candidatePoints[index]; } function matchesFocusAnchor(candidate, focus, match) { if (match === "x") { return candidate.xValue !== void 0 && sameValue(candidate.xValue, focus.primary.xValue); } if (match === "y") { return candidate.yValue !== void 0 && sameValue(candidate.yValue, focus.primary.yValue); } if (match === "series") { return sameValue(candidate.group, focus.primary.group); } if (match === "key") { return candidate.key === focus.primary.key || candidate.datum === focus.primary.datum; } if (match === "group") { return focus.group.some((point) => sameFocusedPoint(candidate, point)); } return sameFocusedPoint(candidate, focus.primary); } const matchesFocusPoint = matchesFocusAnchor; function sameFocusedPoint(left, right) { if (left === right || left.key === right.key) return true; if (!Object.is(left.datum, right.datum)) return false; return isReference(left.datum) || left.datumIndex === right.datumIndex; } function keysRelate(left, right) { return left === right || left.startsWith(`${right}:`) || right.startsWith(`${left}:`); } function sameValue(left, right) { return valueKey(left) === valueKey(right); } function isReference(value) { return typeof value === "object" && value !== null || typeof value === "function"; } function visitNodes(nodes, visit) { for (const node of nodes) { visit(node); if (node.kind === "group") visitNodes(node.children, visit); } } function guideOwnsFocus(guide, focus) { return guide.scope === void 0 || focus.primary.key === guide.scope || focus.primary.key.startsWith(`${guide.scope}:`); } export { focusedNodeKeys, focusedSceneNodes, matchesFocusAnchor, matchesFocusPoint, resolveFocusGuides, resolveFocusPresentation, resolveFocusScene };