@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.
217 lines (216 loc) • 7.29 kB
JavaScript
import { allocateProportionalIntervals } from "./proportional-interval-internal.js";
import { toArray, transformKey, transformValues } from "./transform-internal.js";
function mosaicY(source, options) {
return materializeMosaic(source, options, "y");
}
function mosaicX(source, options) {
return materializeMosaic(source, options, "x");
}
function materializeMosaic(source, options, orientation) {
const owner = orientation === "y" ? "mosaicY" : "mosaicX";
const data = toArray(source);
const xValues = transformValues(data, options.x);
const yValues = transformValues(data, options.y);
const values = transformValues(data, options.value);
const rows = prepareRows(data, xValues, yValues, values, owner);
const xCategories = orderedCategories(
rows,
(row) => row.xValue,
options.xOrder,
owner,
"xOrder"
);
const yCategories = orderedCategories(
rows,
(row) => row.yValue,
options.yOrder,
owner,
"yOrder"
);
const { coordinates, total } = allocateMosaic(
rows,
xCategories,
yCategories,
orientation
);
return rows.map((row) => {
const position = coordinates.get(row.sourceIndex);
const derived = {
xValue: row.xValue,
yValue: row.yValue,
value: row.value,
total,
x: position.x,
x1: position.x1,
x2: position.x2,
y: position.y,
y1: position.y1,
y2: position.y2,
source: [row.datum],
sourceIndexes: [row.sourceIndex]
};
return orientation === "y" ? { ...row.datum, ...derived, xTotal: position.outerTotal } : { ...row.datum, ...derived, yTotal: position.outerTotal };
});
}
function prepareRows(data, xValues, yValues, values, owner) {
const rows = [];
const pairs = /* @__PURE__ */ new Map();
data.forEach((datum, sourceIndex) => {
const value = values[sourceIndex];
if (!isFiniteNumber(value)) return;
if (value < 0) {
throw new TypeError(
`${owner}: value at index ${sourceIndex} must be nonnegative`
);
}
const xValue = xValues[sourceIndex];
const yValue = yValues[sourceIndex];
if (!isChartValue(xValue) || !isChartValue(yValue)) return;
const xIdentity = transformKey(xValue);
const yIdentity = transformKey(yValue);
const pairIdentity = transformKey([xValue, yValue]);
const previous = pairs.get(pairIdentity);
if (previous !== void 0) {
throw new TypeError(
`${owner}: duplicate x/y pair ${formatCategory(xValue)} / ${formatCategory(yValue)} at indexes ${previous} and ${sourceIndex}; aggregate duplicate pairs before calling ${owner}`
);
}
pairs.set(pairIdentity, sourceIndex);
rows.push({
datum,
sourceIndex,
xValue,
yValue,
xIdentity,
yIdentity,
value
});
});
return rows;
}
function orderedCategories(rows, value, explicit, owner, option) {
const categories = [];
const seen = /* @__PURE__ */ new Set();
explicit?.forEach((category, index) => {
if (!isChartValue(category)) {
throw new TypeError(
`${owner}: ${option} category at index ${index} must be a string, finite number, or valid Date`
);
}
const identity = transformKey(category);
if (seen.has(identity)) {
throw new TypeError(
`${owner}: ${option} contains duplicate category ${formatCategory(category)}`
);
}
seen.add(identity);
categories.push({ identity, value: category });
});
for (const row of rows) {
const category = value(row);
const identity = transformKey(category);
if (seen.has(identity)) continue;
seen.add(identity);
categories.push({ identity, value: category });
}
return categories;
}
function allocateMosaic(rows, xCategories, yCategories, orientation) {
const outerCategories = orientation === "y" ? xCategories : yCategories;
const innerCategories = orientation === "y" ? yCategories : xCategories;
const innerRank = new Map(
innerCategories.map((category, index) => [category.identity, index])
);
const outerIdentity = (row) => orientation === "y" ? row.xIdentity : row.yIdentity;
const innerIdentity = (row) => orientation === "y" ? row.yIdentity : row.xIdentity;
const rowsByOuter = /* @__PURE__ */ new Map();
for (const row of rows) {
const outerKey = outerIdentity(row);
let group = rowsByOuter.get(outerKey);
if (!group) {
group = /* @__PURE__ */ new Map();
rowsByOuter.set(outerKey, group);
}
group.set(innerIdentity(row), row);
}
const outerRows = outerCategories.map(
(category) => rowsByOuter.get(category.identity) ?? /* @__PURE__ */ new Map()
);
const rawOuterTotals = outerRows.map(
(group) => sumValues([...group.values()].map((row) => row.value))
);
const outerWeights = overflowSafeGroupWeights(
outerRows.map((group) => [...group.values()].map((row) => row.value)),
rawOuterTotals
);
const outerIntervals = allocateProportionalIntervals(outerWeights);
const total = sumValues(rows.map((row) => row.value));
const coordinates = /* @__PURE__ */ new Map();
outerRows.forEach((group, outerIndex) => {
const outer = outerIntervals[outerIndex];
if (!outer) return;
const innerRows = [...group.values()].sort(
(left, right) => (innerRank.get(innerIdentity(left)) ?? Number.MAX_SAFE_INTEGER) - (innerRank.get(innerIdentity(right)) ?? Number.MAX_SAFE_INTEGER)
);
const innerIntervals = allocateProportionalIntervals(
innerRows.map((row) => row.value)
);
innerRows.forEach((row, innerIndex) => {
const inner = innerIntervals[innerIndex];
if (!inner) return;
const outerCenter = midpoint(outer.start, outer.end);
const innerCenter = midpoint(inner.start, inner.end);
coordinates.set(
row.sourceIndex,
orientation === "y" ? {
x: outerCenter,
x1: outer.start,
x2: outer.end,
y: innerCenter,
y1: inner.start,
y2: inner.end,
outerTotal: rawOuterTotals[outerIndex] ?? 0
} : {
x: innerCenter,
x1: inner.start,
x2: inner.end,
y: outerCenter,
y1: outer.start,
y2: outer.end,
outerTotal: rawOuterTotals[outerIndex] ?? 0
}
);
});
});
return { coordinates, total };
}
function overflowSafeGroupWeights(groups, rawTotals) {
if (rawTotals.every(Number.isFinite)) return [...rawTotals];
let maximum = 0;
for (const group of groups) {
for (const value of group) maximum = Math.max(maximum, value);
}
if (maximum === 0) return rawTotals.map(() => 0);
return groups.map(
(group) => group.reduce((sum, value) => sum + value / maximum, 0)
);
}
function sumValues(values) {
return values.reduce((sum, value) => sum + value, 0);
}
function midpoint(start, end) {
return start + (end - start) / 2;
}
function isChartValue(value) {
return typeof value === "string" || isFiniteNumber(value) || value instanceof Date && Number.isFinite(value.getTime());
}
function isFiniteNumber(value) {
return typeof value === "number" && Number.isFinite(value);
}
function formatCategory(value) {
return value instanceof Date ? value.toISOString() : JSON.stringify(value) ?? String(value);
}
export {
mosaicX,
mosaicY
};