@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.
61 lines (60 loc) • 2.32 kB
JavaScript
const epsilon = 1e-6;
function dodgeOffsets(measuredPositions, radii, padding, edgeAnchored) {
if (measuredPositions.length !== radii.length) {
throw new TypeError("dodge: positions and radii must have equal lengths");
}
const offsets = Array.from({ length: measuredPositions.length }, () => 0);
const order = measuredPositions.map((_position, index) => index);
if (hasVariableRadius(radii)) {
order.sort((left, right) => radii[right] - radii[left] || left - right);
}
const placed = [];
for (const index of order) {
const measured = measuredPositions[index];
const radius = radii[index];
const baseline = edgeAnchored ? radius + padding : 0;
const forbidden = [];
for (const otherIndex of placed) {
const distance = radius + radii[otherIndex] + padding;
const measuredDelta = measured - measuredPositions[otherIndex];
if (Math.abs(measuredDelta) > distance) continue;
const crossDelta = Math.sqrt(
Math.max(0, distance * distance - measuredDelta * measuredDelta)
);
forbidden.push({
minimum: offsets[otherIndex] - crossDelta,
maximum: offsets[otherIndex] + crossDelta
});
}
const candidates = [{ value: baseline, order: 0 }];
forbidden.forEach((interval, intervalIndex) => {
candidates.push(
{ value: interval.minimum, order: intervalIndex * 2 + 1 },
{ value: interval.maximum, order: intervalIndex * 2 + 2 }
);
});
candidates.sort((left, right) => {
const leftRank = edgeAnchored ? left.value : Math.abs(left.value);
const rightRank = edgeAnchored ? right.value : Math.abs(right.value);
return leftRank - rightRank || left.order - right.order;
});
const selected = candidates.find(
(candidate) => (!edgeAnchored || candidate.value >= baseline - epsilon) && forbidden.every(
(interval) => candidate.value <= interval.minimum + epsilon || candidate.value >= interval.maximum - epsilon
)
);
if (!selected) {
throw new TypeError("dodge: could not place a finite circle");
}
offsets[index] = selected.value;
placed.push(index);
}
return offsets;
}
function hasVariableRadius(radii) {
const first = radii[0];
return radii.some((radius) => radius !== first);
}
export {
dodgeOffsets
};