@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.
299 lines (298 loc) • 8.46 kB
JavaScript
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import * as React from "react";
import {
Circle,
ClipPath,
Defs,
G,
Line,
LinearGradient,
Path,
Rect,
Stop,
Svg,
Text
} from "react-native-svg";
function NativeChartSceneNodes({
scene,
nodes,
color,
focusFill,
fontScale,
idPrefix,
resolvePaint
}) {
const gradientIds = React.useMemo(
() => new Set(scene.gradients.map((gradient) => gradient.id)),
[scene.gradients]
);
const paint = React.useCallback(
(value) => resolveScenePaint(
value,
gradientIds,
idPrefix,
resolvePaint,
color,
focusFill
),
[color, focusFill, gradientIds, idPrefix, resolvePaint]
);
return /* @__PURE__ */ jsx(Fragment, { children: nodes.map(
(node) => renderSceneNode(node, idPrefix, paint, positiveFinite(fontScale, 1))
) });
}
const NativeChartScene = React.memo(function NativeChartScene2({
scene,
color,
fontFamily,
fontStyle,
fontStretch,
letterSpacing,
fontScale,
idPrefix,
resolvePaint,
focusFill,
focusPresentation
}) {
const gradientIds = React.useMemo(
() => new Set(scene.gradients.map((gradient) => gradient.id)),
[scene.gradients]
);
const paint = React.useCallback(
(value) => resolveScenePaint(
value,
gradientIds,
idPrefix,
resolvePaint,
color,
focusFill
),
[color, focusFill, gradientIds, idPrefix, resolvePaint]
);
return /* @__PURE__ */ jsxs(
Svg,
{
width: "100%",
height: "100%",
viewBox: `0 0 ${scene.width} ${scene.height}`,
color,
fontFamily,
fontStyle,
fontStretch,
letterSpacing: letterSpacing === void 0 ? void 0 : letterSpacing * positiveFinite(fontScale, 1),
pointerEvents: "none",
accessible: false,
children: [
scene.gradients.length ? /* @__PURE__ */ jsx(Defs, { children: scene.gradients.map((gradient) => /* @__PURE__ */ jsx(
LinearGradient,
{
id: scopedId(idPrefix, gradient.id),
x1: percent(gradient.x1 ?? 0),
y1: percent(gradient.y1 ?? 1),
x2: percent(gradient.x2 ?? 0),
y2: percent(gradient.y2 ?? 0),
children: gradient.stops.map((stop, index) => /* @__PURE__ */ jsx(
Stop,
{
offset: percent(stop.offset),
stopColor: paint(stop.color),
stopOpacity: stop.opacity
},
`${gradient.id}:${index}`
))
},
gradient.id
)) }) : null,
scene.theme.background === "transparent" ? null : /* @__PURE__ */ jsx(
Rect,
{
x: 0,
y: 0,
width: scene.width,
height: scene.height,
fill: paint(scene.theme.background)
}
),
focusPresentation?.under.map(
(node) => renderSceneNode(node, idPrefix, paint, positiveFinite(fontScale, 1))
),
scene.nodes.map(
(node) => renderSceneNode(node, idPrefix, paint, positiveFinite(fontScale, 1))
),
focusPresentation?.over.map(
(node) => renderSceneNode(node, idPrefix, paint, positiveFinite(fontScale, 1))
)
]
}
);
});
function renderSceneNode(node, idPrefix, paint, fontScale) {
if (node.kind === "group" && node.focus) return null;
const style = nativeSceneStyle(node.style, paint);
switch (node.kind) {
case "group":
return renderGroup(node, idPrefix, paint, style, fontScale);
case "rule":
return /* @__PURE__ */ jsx(
Line,
{
...style,
x1: node.x1,
y1: node.y1,
x2: node.x2,
y2: node.y2
},
node.key
);
case "polyline":
return /* @__PURE__ */ jsx(
Path,
{
...style,
d: node.path ?? pointsPath(node.points, false),
vectorEffect: "non-scaling-stroke"
},
node.key
);
case "area":
return /* @__PURE__ */ jsx(
Path,
{
...style,
d: node.polygons !== void 0 ? polygonsPath(node.polygons) : node.path ?? pointsPath(node.points, true),
fillRule: node.polygons === void 0 ? void 0 : "evenodd",
vectorEffect: "non-scaling-stroke"
},
node.key
);
case "dot":
return /* @__PURE__ */ jsx(
Circle,
{
...style,
cx: node.x,
cy: node.y,
r: node.radius
},
node.key
);
case "rect":
return /* @__PURE__ */ jsx(
Rect,
{
...style,
x: node.x,
y: node.y,
width: node.width,
height: node.height,
rx: node.radius
},
node.key
);
case "label":
return /* @__PURE__ */ jsx(
Text,
{
...style,
x: node.x,
y: node.y,
textAnchor: node.anchor,
alignmentBaseline: node.baseline === "auto" ? "baseline" : node.baseline,
transform: node.rotate === void 0 ? void 0 : `rotate(${node.rotate} ${node.x} ${node.y})`,
fontSize: (node.fontSize ?? 16) * fontScale,
fontWeight: node.fontWeight,
children: node.text
},
node.key
);
}
}
function renderGroup(node, idPrefix, paint, style, fontScale) {
const clipId = node.clip ? scopedId(idPrefix, `clip-${stableId(node.key)}`) : void 0;
const transform = node.translateX === void 0 && node.translateY === void 0 ? void 0 : `translate(${node.translateX ?? 0} ${node.translateY ?? 0})`;
return /* @__PURE__ */ jsxs(
G,
{
...style,
transform,
clipPath: clipId ? `url(#${clipId})` : void 0,
children: [
node.clip && clipId ? /* @__PURE__ */ jsx(Defs, { children: /* @__PURE__ */ jsx(ClipPath, { id: clipId, children: /* @__PURE__ */ jsx(
Rect,
{
x: node.clip.x,
y: node.clip.y,
width: node.clip.width,
height: node.clip.height
}
) }) }) : null,
node.children.map(
(child) => renderSceneNode(child, idPrefix, paint, fontScale)
)
]
},
node.key
);
}
function nativeSceneStyle(style, paint) {
if (!style) return {};
return {
fill: style.fill === void 0 ? void 0 : paint(style.fill),
fillOpacity: style.fillOpacity,
stroke: style.stroke === void 0 ? void 0 : paint(style.stroke),
strokeOpacity: style.strokeOpacity,
strokeWidth: style.strokeWidth,
opacity: style.opacity,
strokeLinecap: style.lineCap,
strokeLinejoin: resolveNativeLineJoin(style.lineJoin),
strokeDasharray: style.strokeDasharray
};
}
function resolveNativeLineJoin(lineJoin) {
if (lineJoin === "arcs") return "round";
if (lineJoin === "miter-clip") return "miter";
return lineJoin;
}
function resolveScenePaint(value, gradientIds, idPrefix, resolvePaint, color, canvas) {
const match = /^url\(#([\s\S]*)\)$/.exec(value);
const id = match?.[1];
if (id !== void 0 && gradientIds.has(id)) {
return `url(#${scopedId(idPrefix, id)})`;
}
return resolvePaint(value, { color, canvas });
}
function pointsPath(points, close) {
return `${points.map(([x, y], index) => `${index === 0 ? "M" : "L"}${x},${y}`).join("")}${close ? "Z" : ""}`;
}
function polygonsPath(polygons) {
return polygons.flatMap((polygon) => polygon).filter((ring) => ring.length > 0).map((ring) => pointsPath(ring, true)).join("");
}
function scopedId(prefix, id) {
const encodedId = encodeResourceId(id);
return prefix ? `${prefix}-${encodedId}` : encodedId;
}
function encodeResourceId(value) {
if (!value) return "_";
return Array.from(
value,
(character) => /^[a-zA-Z0-9-]$/.test(character) ? character : `_x${character.codePointAt(0).toString(16)}_`
).join("");
}
function stableId(value) {
let hash = 2166136261;
for (let index = 0; index < value.length; index += 1) {
hash = Math.imul(hash ^ value.charCodeAt(index), 16777619);
}
return (hash >>> 0).toString(36);
}
function percent(value) {
return `${Math.max(0, Math.min(1, value)) * 100}%`;
}
function positiveFinite(value, fallback) {
return value !== void 0 && Number.isFinite(value) && value > 0 ? value : fallback;
}
export {
NativeChartScene,
NativeChartSceneNodes,
resolveNativeLineJoin
};