@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.
100 lines (99 loc) • 3.36 kB
JavaScript
import { orderedIndexes, toArray, transformValues } from "./transform-internal.js";
import { allocateProportionalIntervals } from "./proportional-interval-internal.js";
const tau = Math.PI * 2;
const fullRevolutionTolerance = 1e-12;
function pie(source, options) {
const data = toArray(source);
const values = transformValues(data, options.value);
const startAngle = options.startAngle ?? 0;
const endAngle = options.endAngle ?? tau;
const gapAngle = options.gapAngle ?? 0;
assertFinite(startAngle, "startAngle");
assertFinite(endAngle, "endAngle");
assertNonnegativeFinite(gapAngle, "gapAngle");
const sweep = endAngle - startAngle;
if (!Number.isFinite(sweep) || Math.abs(sweep) > tau) {
throw new TypeError("pie: angular sweep must be no greater than 2\u03C0");
}
const sourceIndexes = values.flatMap((value, sourceIndex) => {
if (!isFiniteNumber(value)) return [];
if (value < 0) {
throw new TypeError(
`pie: value at index ${sourceIndex} must be nonnegative`
);
}
return [sourceIndex];
});
const ordered = orderedIndexes(
data,
sourceIndexes,
options.orderBy,
options.order
);
const completeRevolution = Math.abs(Math.abs(sweep) - tau) <= fullRevolutionTolerance;
assertPieGapCapacity(ordered, values, sweep, gapAngle, completeRevolution);
const allocated = allocateProportionalIntervals(
ordered.map((sourceIndex) => values[sourceIndex]),
{
start: startAngle,
end: endAngle,
gap: gapAngle,
gapAfterLast: completeRevolution
}
);
const intervals = /* @__PURE__ */ new Map();
ordered.forEach((sourceIndex, index) => {
const interval = allocated[index];
const value = values[sourceIndex];
intervals.set(sourceIndex, {
value,
index,
fraction: interval.fraction,
startAngle: interval.start,
endAngle: interval.end,
angle: interval.start + (interval.end - interval.start) / 2,
padAngle: 0
});
});
return sourceIndexes.map((sourceIndex) => {
const datum = data[sourceIndex];
return {
...datum,
...intervals.get(sourceIndex),
source: [datum],
sourceIndexes: [sourceIndex]
};
});
}
function assertPieGapCapacity(ordered, values, sweep, gapAngle, completeRevolution) {
const positiveCount = ordered.reduce(
(count, sourceIndex) => count + (values[sourceIndex] > 0 ? 1 : 0),
0
);
const absoluteSweep = Math.abs(sweep);
const gapCount = positiveCount === 0 ? 0 : completeRevolution ? positiveCount : Math.max(0, positiveCount - 1);
const totalGap = gapCount * gapAngle;
if (!Number.isFinite(totalGap) || totalGap > absoluteSweep) {
throw new TypeError("pie: gapAngle leaves insufficient angular space");
}
const drawableSweep = absoluteSweep - totalGap;
if (positiveCount > 0 && drawableSweep <= 0) {
throw new TypeError("pie: positive values require drawable angular space");
}
}
function isFiniteNumber(value) {
return typeof value === "number" && Number.isFinite(value);
}
function assertFinite(value, name) {
if (!Number.isFinite(value)) {
throw new TypeError(`pie: ${name} must be finite`);
}
}
function assertNonnegativeFinite(value, name) {
if (!Number.isFinite(value) || value < 0) {
throw new TypeError(`pie: ${name} must be nonnegative and finite`);
}
}
export {
pie
};