@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.
200 lines (199 loc) • 6.7 kB
JavaScript
import {
createScenePointLookup,
sceneNodeOwnedPoints
} from "./scene-point-ownership-internal.js";
import { matchesFocusAnchor } from "./focus-layer.js";
function resolveMarkStateScene(scene, focus, pointer = null) {
if (!focus || !sceneHasMarkStates(scene.nodes)) return { scene };
let transition;
const transitions = {};
const visit = (nodes2, inheritedPoints, definitions, data, inheritedLookup) => nodes2.map((node) => {
const state = node.kind === "group" ? node.states : void 0;
const points = state?.points ?? inheritedPoints;
const nodeDefinitions = state?.definitions ?? definitions;
const nodeData = state?.data ?? data;
const lookup = state ? createScenePointLookup(state.points) : inheritedLookup;
const candidates = points ? lookup ? sceneNodeOwnedPoints(node, points, lookup) : points : emptyPoints;
const resolved = node.kind !== "group" && nodeDefinitions && nodeData && candidates.length ? resolveNodeState(
node,
candidates,
nodeData,
nodeDefinitions,
focus,
pointer
) : { node };
if (resolved.transition) {
transition = mergeTransition(transition, resolved.transition);
for (const point of candidates) {
transitions[point.markId] = mergeTransition(
transitions[point.markId],
resolved.transition
);
}
}
const next = resolved.node;
return next.kind === "group" ? {
...next,
children: visit(
next.children,
candidates.length ? candidates : points,
nodeDefinitions,
nodeData,
lookup
)
} : next;
});
const nodes = visit(scene.nodes);
return {
scene: { ...scene, nodes },
transition,
...Object.keys(transitions).length ? { transitions } : {}
};
}
function sceneHasMarkStates(nodes) {
return nodes.some(
(node) => node.kind === "group" && (node.states !== void 0 || sceneHasMarkStates(node.children))
);
}
function resolveNodeState(node, candidates, data, definitions, focus, pointer) {
let output = node;
let transition;
for (const definition of definitions) {
const context = matchingContext(
candidates,
data,
definition,
focus,
pointer
);
if (!context) continue;
output = applyStateStyle(output, definition.style, context);
if (definition.transition) {
transition = mergeTransition(transition, definition.transition);
}
}
return { node: output, transition };
}
function matchingContext(candidates, data, definition, focus, pointer) {
if (typeof definition.when !== "function" && definition.when.focus === "unmatched" && candidates.some((point) => matchesFocusAnchor(point, focus, "group"))) {
return void 0;
}
for (const point of candidates) {
const context = {
datum: point.datum,
index: point.datumIndex,
data,
point,
focus,
pointer,
matches: (match) => matchesFocusAnchor(point, focus, match)
};
const matches = typeof definition.when === "function" ? definition.when(context) : matchesSelector(definition.when, context);
if (matches) return context;
}
return void 0;
}
function matchesSelector(selector, context) {
const source = selector.source;
if (source !== void 0 && !(Array.isArray(source) ? source.includes(context.focus.source) : source === context.focus.source)) {
return false;
}
if (selector.pinned !== void 0 && selector.pinned !== context.focus.pinned) {
return false;
}
return selector.focus === "unmatched" ? !context.matches("group") : context.matches(selector.focus);
}
function applyStateStyle(node, definition, context) {
const style = { ...node.style };
for (const property of styleProperties) {
const value = resolveValue(definition[property], context);
if (value !== void 0)
style[property] = value;
}
let output = { ...node, style };
const dx = resolveValue(definition.dx, context) ?? 0;
const dy = resolveValue(definition.dy, context) ?? 0;
const r = resolveValue(definition.r, context);
const radius = resolveValue(definition.radius, context);
const inset = resolveValue(definition.inset, context);
const fontSize = resolveValue(definition.fontSize, context);
const fontWeight = resolveValue(definition.fontWeight, context);
const rotate = resolveValue(definition.rotate, context);
switch (output.kind) {
case "dot":
output = {
...output,
x: output.x + dx,
y: output.y + dy,
radius: r ?? output.radius
};
break;
case "rect": {
const currentInset = output.inset ?? 0;
let nextInset = Math.max(0, inset ?? currentInset);
if (Number.isFinite(output.maxThickness) && (output.insetAxis === "x" || output.insetAxis === "y")) {
const currentThickness = output.insetAxis === "x" ? output.width : output.height;
const bandThickness = currentThickness + currentInset * 2;
const requestedThickness = Math.max(0, bandThickness - nextInset * 2);
const cappedThickness = Math.min(
requestedThickness,
Math.max(0, output.maxThickness)
);
nextInset = (bandThickness - cappedThickness) / 2;
}
const amount = nextInset - currentInset;
const insetX = output.insetAxis !== "y" ? amount : 0;
const insetY = output.insetAxis !== "x" ? amount : 0;
output = {
...output,
x: output.x + insetX + dx,
y: output.y + insetY + dy,
width: Math.max(0, output.width - insetX * 2),
height: Math.max(0, output.height - insetY * 2),
radius: radius ?? output.radius,
inset: nextInset
};
break;
}
case "label":
output = {
...output,
x: output.x + dx,
y: output.y + dy,
fontSize: fontSize ?? output.fontSize,
fontWeight: fontWeight ?? output.fontWeight,
rotate: rotate ?? output.rotate
};
break;
}
return output;
}
const styleProperties = [
"fill",
"fillOpacity",
"stroke",
"strokeOpacity",
"strokeWidth",
"opacity",
"strokeDasharray"
];
function resolveValue(value, context) {
return typeof value === "function" ? value(context) : value;
}
const emptyPoints = [];
function mergeTransition(current, next) {
if (!current || current.type !== next.type) return next;
if (current.type === "spring" && next.type === "spring") {
return { ...current, ...next };
}
if (current.type !== "tween" || next.type !== "tween") return next;
return {
...current,
...next,
duration: Math.max(current.duration ?? 250, next.duration ?? 250)
};
}
export {
resolveMarkStateScene,
sceneHasMarkStates
};