@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.
189 lines (188 loc) • 6.59 kB
JavaScript
import { reconcileChartSvg, reconcileChartSvgFragment } from "./reconcile.js";
import { renderChartSvg } from "./svg.js";
import { focusedNodeKeys, resolveFocusScene } from "./focus-layer.js";
import { resolveFocusGuides } from "./focus-presentation.js";
import { renderFocusGuideLayer } from "./svg-renderer.js";
import { renderFocusGuideLayerWithRenderer } from "./svg-focus-guide-serializer.js";
import {
detachSvgFocusGuideLayers,
ensureSvgFocusGuideLayer,
removeSvgFocusGuideLayer,
restoreSvgFocusGuideLayers
} from "./svg-focus-guide-layer.js";
import { resolveMarkStateScene } from "./mark-state.js";
import { resolveMarkStateTransition } from "./mark-state-transition.js";
import { viewportTranslationChanged } from "./scene-point-map.js";
import { svgClientToScene } from "./svg-coordinates.js";
function createSvgChartRenderer(renderSvg = renderChartSvg) {
const renderer = {
id: "svg",
prerender: renderSvg,
mount(container) {
let cancelAnimation = () => {
};
let cancelFocusAnimation = () => {
};
let scene;
let renderOptions;
let stateTransition;
let markStatePainted = false;
let retargetedFocus = false;
const svgElement = () => {
const svg = container.querySelector("svg.ts-chart");
if (!svg) {
throw new Error(
"The SVG renderer must produce an svg.ts-chart root element."
);
}
return svg;
};
const surface = {
renderer,
get element() {
return svgElement();
},
render(nextScene, options) {
const viewportMoved = Boolean(
scene && viewportTranslationChanged(scene, nextScene)
);
cancelAnimation();
cancelFocusAnimation();
cancelFocusAnimation = () => {
};
const retainsFocusGuideLayers = Boolean(scene?.focusGuides?.length);
const focusGuideLayers = retainsFocusGuideLayers ? detachSvgFocusGuideLayers(svgElement()) : {};
cancelAnimation = reconcileChartSvg(
container,
renderSvg(nextScene, options),
viewportMoved ? void 0 : options.animation
);
if (retainsFocusGuideLayers) {
restoreSvgFocusGuideLayers(
svgElement(),
focusGuideLayers,
(placement) => nextScene.focusGuides?.some(
(guide) => guide.placement === placement
) === true
);
}
scene = nextScene;
renderOptions = options;
stateTransition = void 0;
markStatePainted = false;
retargetedFocus = false;
},
clientToScene(scene2, clientX, clientY) {
return svgClientToScene(svgElement(), scene2, clientX, clientY);
},
paintFocus(focus, pointer, cursor) {
if (!scene || !renderOptions) return;
const state = resolveMarkStateScene(scene, focus, pointer);
const resolved = resolveFocusScene(state.scene, focus);
const previousTransition = stateTransition;
if (resolved.scene !== scene || markStatePainted || retargetedFocus || previousTransition) {
cancelFocusAnimation();
cancelFocusAnimation = () => {
};
const focusGuideLayers = detachSvgFocusGuideLayers(svgElement());
cancelAnimation();
cancelAnimation = reconcileChartSvg(
container,
renderSvg(resolved.scene, renderOptions),
resolveMarkStateTransition(
state.transition ?? previousTransition,
container
)
);
restoreSvgFocusGuideLayers(svgElement(), focusGuideLayers);
}
retargetedFocus = resolved.retargeted;
markStatePainted = Boolean(focus && state.scene !== scene);
stateTransition = focus ? state.transition ?? previousTransition : void 0;
paintSvgFocus(svgElement(), resolved.scene, focus);
cancelFocusAnimation();
cancelFocusAnimation = paintSvgFocusGuides(
svgElement(),
resolved.scene,
focus,
pointer,
cursor,
renderOptions,
renderSvg
);
return resolved.scene;
},
destroy() {
cancelAnimation();
cancelFocusAnimation();
}
};
return surface;
}
};
return renderer;
}
const svgChartRenderer = createSvgChartRenderer();
function paintSvgFocus(svg, scene, focus) {
const sceneLayers = collectFocusLayers(scene.nodes);
const elements = svg.querySelectorAll(
"[data-ts-focus-layer]:not([data-ts-focus-guide-layer])"
);
elements.forEach((element, index) => {
const layer = sceneLayers[index];
const visible = layer ? focusedNodeKeys(layer, focus) : /* @__PURE__ */ new Set();
element.setAttribute(
"visibility",
focus && visible.size ? "visible" : "hidden"
);
element.querySelectorAll("[data-ts-key]").forEach((child) => {
const key = child.dataset.tsKey;
child.setAttribute(
"visibility",
key && visible.has(key) ? "visible" : "hidden"
);
});
});
}
function paintSvgFocusGuides(svg, scene, focus, pointer, cursor, renderOptions, renderSvg) {
const presentation = resolveFocusGuides(scene, focus, pointer, cursor);
const cancellations = [];
for (const placement of ["under", "over"]) {
if (!scene.focusGuides?.some((guide) => guide.placement === placement)) {
removeSvgFocusGuideLayer(svg, placement);
continue;
}
const layer = ensureSvgFocusGuideLayer(svg, placement);
const nodes = presentation[placement];
if (!nodes.length) {
layer.setAttribute("visibility", "hidden");
continue;
}
const markup = renderSvg === renderChartSvg ? renderFocusGuideLayer(nodes, placement, renderOptions.idPrefix ?? "") : renderFocusGuideLayerWithRenderer(
svg,
scene,
nodes,
placement,
renderOptions,
renderSvg
);
cancellations.push(reconcileChartSvgFragment(layer, markup));
}
return () => cancellations.forEach((cancel) => cancel());
}
function collectFocusLayers(nodes) {
const layers = [];
for (const node of nodes) {
if (node.kind !== "group") continue;
if (node.focus) {
layers.push(node);
} else {
layers.push(...collectFocusLayers(node.children));
}
}
return layers;
}
export {
createSvgChartRenderer,
svgChartRenderer
};