@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.
185 lines (184 loc) • 6.11 kB
JavaScript
import { valueKey } from "./scales.js";
function createInteractionAxis(options) {
const { axis, scale, sample } = options;
if (!scale) throw new TypeError(`A ${axis}-axis interaction requires a scale`);
assertValue(sample, `The ${axis}-axis interaction value`);
const minimum = Math.min(...options.extent);
const maximum = Math.max(...options.extent);
if (!Number.isFinite(minimum) || !Number.isFinite(maximum)) {
throw new TypeError(`The ${axis}-axis interaction extent must be finite`);
}
const extent = [minimum, maximum];
const values = options.values?.map(cloneValue);
const keys = values?.map(valueKey);
let positions;
if (values) {
if (!values.length) {
throw new TypeError(
`The ${axis}-axis interaction values must not be empty`
);
}
const expectedKind = valueKind(sample);
const unique = /* @__PURE__ */ new Set();
positions = values.map((value, index) => {
assertValue(value, `The ${axis}-axis interaction value at index ${index}`);
if (valueKind(value) !== expectedKind) {
throw new TypeError(
`The ${axis}-axis interaction values must use one value type`
);
}
const key = keys[index];
if (unique.has(key)) {
throw new TypeError(
`The ${axis}-axis interaction values must be unique`
);
}
unique.add(key);
return mappedPosition(scale, value, axis);
});
assertMonotonePositions(positions, axis);
} else {
const kind = valueKind(sample);
if (kind === "string") {
throw new TypeError(
`A string ${axis}-axis interaction requires explicit values`
);
}
if (!scale.invert) {
throw new TypeError(
`A continuous ${axis}-axis interaction requires an invertible scale or explicit values`
);
}
}
const clampPosition = (position) => Math.max(minimum, Math.min(maximum, position));
const indexOf = (value) => keys?.indexOf(valueKey(value)) ?? -1;
const at = (index) => {
if (!values?.length) {
throw new TypeError(
`The ${axis}-axis interaction requires explicit values for indexed movement`
);
}
const bounded = Math.max(0, Math.min(values.length - 1, index));
return cloneValue(values[bounded]);
};
const invert = (position) => {
if (values || !scale.invert) {
throw new TypeError(
`The ${axis}-axis interaction requires a continuous scale inversion`
);
}
if (!Number.isFinite(position)) {
throw new TypeError(
`The ${axis}-axis interaction position must be finite`
);
}
const value = scale.invert(position);
assertValue(value, `The ${axis}-axis scale inversion`);
if (valueKind(value) !== valueKind(sample)) {
throw new TypeError(
`The ${axis}-axis scale inversion returned a different value type`
);
}
return cloneValue(value);
};
return {
scale,
extent,
...values ? { values } : {},
...positions ? { positions } : {},
position(value) {
return mappedPosition(scale, value, axis);
},
invert,
valueAt(position) {
const bounded = clampPosition(position);
if (values && positions) {
let nearestIndex = 0;
let nearestDistance = Number.POSITIVE_INFINITY;
positions.forEach((candidate, index) => {
const distance = Math.abs(candidate - bounded);
if (distance < nearestDistance) {
nearestIndex = index;
nearestDistance = distance;
}
});
return cloneValue(values[nearestIndex]);
}
return invert(bounded);
},
order(first, second) {
if (values) {
const firstIndex = indexOf(first);
const secondIndex = indexOf(second);
if (firstIndex < 0 || secondIndex < 0) {
throw new TypeError(
`The ${axis}-axis interaction range must use an explicit value`
);
}
return firstIndex <= secondIndex ? [cloneValue(first), cloneValue(second)] : [cloneValue(second), cloneValue(first)];
}
return numericValue(first) <= numericValue(second) ? [cloneValue(first), cloneValue(second)] : [cloneValue(second), cloneValue(first)];
},
indexOf,
at,
step(value, amount) {
const index = indexOf(value);
if (index < 0) {
throw new TypeError(
`The ${axis}-axis interaction value is not in its explicit values`
);
}
return at(index + amount);
},
clampPosition,
layoutKey: valueKey
};
}
function assertMonotonePositions(positions, axis) {
if (positions.length < 2) return;
const firstDelta = positions[1] - positions[0];
if (firstDelta === 0) nonmonotone(axis);
const direction = Math.sign(firstDelta);
for (let index = 2; index < positions.length; index += 1) {
const delta = positions[index] - positions[index - 1];
if (!delta || Math.sign(delta) !== direction) nonmonotone(axis);
}
}
function nonmonotone(axis) {
throw new TypeError(
`The ${axis}-axis interaction values must map to strictly monotone positions`
);
}
function mappedPosition(scale, value, axis) {
const position = scale.map(value);
if (!Number.isFinite(position)) {
throw new TypeError(
`The ${axis}-axis interaction value must map to a finite position`
);
}
return position;
}
function valueKind(value) {
return value instanceof Date ? "date" : typeof value;
}
function numericValue(value) {
return value instanceof Date ? value.getTime() : Number(value);
}
function assertValue(value, label) {
if (value instanceof Date) {
if (Number.isFinite(value.getTime())) return;
throw new TypeError(`${label} must be a valid Date`);
}
if (typeof value === "number") {
if (Number.isFinite(value)) return;
throw new TypeError(`${label} must be finite`);
}
if (typeof value === "string") return;
throw new TypeError(`${label} must be a chart value`);
}
function cloneValue(value) {
return value instanceof Date ? new Date(value.getTime()) : value;
}
export {
createInteractionAxis
};