@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.
277 lines (276 loc) • 9.48 kB
JavaScript
import {} from "./cursor-host-contract.js";
import { valueKey } from "./scales.js";
import {
chartPointFromNavigationOrder,
chartPointFromSceneOrder,
resolveChartFocusStrategy,
resolveChartPointerFocus,
restoreChartFocusPoint,
sameChartPointIdentity
} from "./interaction.js";
function createChartCursor(initialState = null) {
validateCursorState(initialState);
let state = initialState;
const listeners = /* @__PURE__ */ new Set();
return {
getState: () => state,
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
setState(next) {
const resolved = typeof next === "function" ? next(state) : next;
validateCursorState(resolved);
if (Object.is(resolved, state)) return;
state = resolved;
for (const listener of [...listeners]) listener();
}
};
}
const cursorHost = {
id: "cursor",
__chartExtensionType: "cursor",
create(controller) {
let publishedState;
let publishing = false;
return {
controller,
getState: () => controller.getState(),
subscribe: (listener) => controller.subscribe(() => {
if (publishing) {
publishedState = controller.getState() ?? void 0;
}
listener();
}),
owns: (state) => state !== null && state === publishedState,
publish(state) {
publishedState = state;
publishing = true;
try {
controller.setState(state);
} finally {
publishedState = controller.getState() ?? void 0;
publishing = false;
}
},
clearOwnedTransient() {
const current = controller.getState();
if (!current || current !== publishedState || current.pinned) {
return false;
}
publishedState = void 0;
controller.setState(null);
return true;
},
clear() {
publishedState = void 0;
controller.setState(null);
},
destroy() {
const current = controller.getState();
if (current && current === publishedState && !current.pinned) {
controller.setState(null);
}
publishedState = void 0;
},
resolvePresentation: resolveChartCursorPresentation,
resolveFocus: resolveChartCursorFocus,
createFocusState: createFocusChartCursorState,
createFreeState: createFreeChartCursorState
};
}
};
function resolveChartCursorPresentation(scene, binding, state) {
if (!state) return null;
const match = binding.mode === "focus" ? binding.match ?? "xy" : "xy";
const x = match === "y" ? void 0 : resolveCursorAxis(scene, binding, state, "x");
const y = match === "x" ? void 0 : resolveCursorAxis(scene, binding, state, "y");
return x || y || state.anchor === "value" ? { state, axes: match, x, y } : null;
}
function createFreeChartCursorState(scene, binding, position, source = "pointer", pinned = false) {
const normalized = {
x: normalizePosition(position.x, scene.chart.x, scene.chart.width),
y: normalizePosition(position.y, scene.chart.y, scene.chart.height)
};
const base = {
anchor: "normalized",
scene: position,
normalized,
source,
pinned
};
const presentation = resolveChartCursorPresentation(scene, binding, base);
const value = cursorValues(presentation?.x?.value, presentation?.y?.value);
return value ? { ...base, value } : base;
}
function createFocusChartCursorState(scene, binding, focus) {
const match = binding.match ?? "xy";
const point = focus.primary;
if (match === "x") {
return {
anchor: "value",
value: { x: point.xValue },
scene: { x: point.x },
normalized: {
x: normalizePosition(point.x, scene.chart.x, scene.chart.width)
},
group: point.group,
origin: cursorPointIdentity(point),
source: focus.source,
pinned: focus.pinned
};
}
if (match === "y") {
return {
anchor: "value",
value: { y: point.yValue },
scene: { y: point.y },
normalized: {
y: normalizePosition(point.y, scene.chart.y, scene.chart.height)
},
group: point.group,
origin: cursorPointIdentity(point),
source: focus.source,
pinned: focus.pinned
};
}
return {
anchor: "value",
value: { x: point.xValue, y: point.yValue },
scene: { x: point.x, y: point.y },
normalized: {
x: normalizePosition(point.x, scene.chart.x, scene.chart.width),
y: normalizePosition(point.y, scene.chart.y, scene.chart.height)
},
group: point.group,
origin: cursorPointIdentity(point),
source: focus.source,
pinned: focus.pinned
};
}
function resolveChartCursorFocus(points, binding, state, strategy) {
if (state?.anchor !== "value") return [];
const match = binding.match ?? "xy";
const xValue = state.value.x;
const yValue = state.value.y;
if ((match === "x" || match === "xy") && xValue === void 0) return [];
if ((match === "y" || match === "xy") && yValue === void 0) return [];
const matches = points.filter(
(point2) => (match === "y" || sameChartValue(point2.xValue, xValue)) && (match === "x" || sameChartValue(point2.yValue, yValue))
);
const groupedMatches = state.group === void 0 ? matches : matches.filter(
(candidate) => sameChartValue(candidate.group, state.group)
);
const candidates = groupedMatches.length ? groupedMatches : matches;
const origin = state.origin;
const point = (origin ? cursorPointFromIdentity(candidates, origin) : void 0) ?? candidates[0];
return point ? strategy?.group(points, { point }) ?? [point] : [];
}
function cursorPointIdentity(point) {
return {
key: point.key,
markId: point.markId,
datumIndex: point.datumIndex
};
}
function cursorPointFromIdentity(points, identity) {
const keyed = points.filter(
(point) => point.key === identity.key && point.markId === identity.markId
);
if (keyed.length === 1) return keyed[0];
if (keyed.length > 1) {
return keyed.find((point) => point.datumIndex === identity.datumIndex) ?? keyed[0];
}
return void 0;
}
function sameChartValue(left, right) {
return valueKey(left) === valueKey(right);
}
function resolveCursorAxis(scene, binding, state, axis) {
const origin = axis === "x" ? scene.chart.x : scene.chart.y;
const length = axis === "x" ? scene.chart.width : scene.chart.height;
let position;
let normalized;
let value;
if (state.anchor === "scene") {
const coordinate = state.scene[axis];
if (typeof coordinate !== "number" || !Number.isFinite(coordinate)) {
return void 0;
}
position = coordinate;
normalized = normalizePosition(position, origin, length);
} else if (state.anchor === "normalized") {
const coordinate = state.normalized[axis];
if (typeof coordinate !== "number" || !Number.isFinite(coordinate)) {
return void 0;
}
normalized = coordinate;
position = origin + normalized * length;
} else {
value = state.value[axis];
if (value === void 0) return void 0;
const scale = scene.scales[axis];
if (!scale || scale.type === "none") return void 0;
position = (scale.viewport?.map ?? scale.map)(value);
if (!Number.isFinite(position)) return void 0;
normalized = normalizePosition(position, origin, length);
}
if (state.anchor !== "value") {
const options = binding.mode === "free" ? binding[axis] : void 0;
value = options?.valueAt ? options.valueAt({ axis, scene, position, normalized }) : invertFreeCursorAxis(scene, axis, position);
}
return { position, normalized, value };
}
function invertFreeCursorAxis(scene, axis, position) {
const scale = scene.scales[axis];
if (!scale || scale.type === "none") {
throw new TypeError(
`A free chart cursor requires an ${axis} scale or an explicit ${axis}.valueAt callback`
);
}
if (!scale.invert) {
throw new TypeError(
`A free chart cursor requires an invertible ${axis} scale or an explicit ${axis}.valueAt callback`
);
}
const value = scale.invert(position);
if (typeof value === "number" && !Number.isFinite(value) || value instanceof Date && !Number.isFinite(value.getTime())) {
throw new TypeError(
`The free chart cursor ${axis} scale inversion returned an invalid value`
);
}
return value instanceof Date ? new Date(value.getTime()) : value;
}
function cursorValues(x, y) {
return x !== void 0 ? y !== void 0 ? { x, y } : { x } : y !== void 0 ? { y } : void 0;
}
function normalizePosition(position, origin, length) {
return (position - origin) / length;
}
function validateCursorState(state) {
if (!state) return;
const coordinates = state[state.anchor];
if (!coordinates || coordinates.x === void 0 && coordinates.y === void 0) {
throw new TypeError(
"A chart cursor requires at least one anchor coordinate"
);
}
if (state.anchor !== "value" && (coordinates.x !== void 0 && !Number.isFinite(coordinates.x) || coordinates.y !== void 0 && !Number.isFinite(coordinates.y))) {
throw new TypeError("Chart cursor coordinates must be finite numbers");
}
}
export {
chartPointFromNavigationOrder,
chartPointFromSceneOrder,
createChartCursor,
createFocusChartCursorState,
createFreeChartCursorState,
cursorHost,
resolveChartCursorFocus,
resolveChartCursorPresentation,
resolveChartFocusStrategy,
resolveChartPointerFocus,
restoreChartFocusPoint,
sameChartPointIdentity,
sameChartValue
};