chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
// At 11px tabular-nums, 1 char ≈ 6.5px, so character count is a width proxy (see useYAxisWidth).
|
||||
const PX_PER_CH = 6.5;
|
||||
// Minimum gap between adjacent labels.
|
||||
const LABEL_GAP_PX = 16;
|
||||
// Floor so very short labels still get some space.
|
||||
const MIN_LABEL_PX = 24;
|
||||
|
||||
/** Pick `count` indices evenly spaced across [0, n), always including the first and last. */
|
||||
export function selectEvenlySpacedIndices(n: number, count: number): number[] {
|
||||
if (n <= 0) return [];
|
||||
if (count <= 1) return [0];
|
||||
if (count >= n) return Array.from({ length: n }, (_, i) => i);
|
||||
if (count === 2) return [0, n - 1];
|
||||
|
||||
const step = (n - 1) / (count - 1);
|
||||
const out: number[] = [];
|
||||
const seen = new Set<number>();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const idx = Math.round(i * step);
|
||||
if (!seen.has(idx)) {
|
||||
seen.add(idx);
|
||||
out.push(idx);
|
||||
}
|
||||
}
|
||||
// Rounding can drop the final index; force it in.
|
||||
if (!seen.has(n - 1)) out.push(n - 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Pick `maxLabels` values evenly spaced across `values`, always including the first and last. */
|
||||
export function selectEvenlySpacedTicks<T>(values: T[], maxLabels: number): T[] {
|
||||
return selectEvenlySpacedIndices(values.length, maxLabels).map((i) => values[i]);
|
||||
}
|
||||
|
||||
/** How many labels of `maxLabelChars` width fit in `width` pixels. */
|
||||
export function estimateMaxLabels(width: number, maxLabelChars: number): number {
|
||||
if (!width || width <= 0) return 0;
|
||||
const labelPx = Math.max(MIN_LABEL_PX, maxLabelChars * PX_PER_CH) + LABEL_GAP_PX;
|
||||
return Math.max(1, Math.floor(width / labelPx));
|
||||
}
|
||||
|
||||
/**
|
||||
* From `indices` (into `labels`/`values`), return the tick values with adjacent
|
||||
* duplicate labels dropped. The final index is always kept: if it repeats the
|
||||
* previous label it replaces that tick instead of being skipped, so the
|
||||
* end-of-range label never disappears (honoring the "first + last" contract).
|
||||
*/
|
||||
export function dedupeTicksByLabel<T>(indices: number[], labels: string[], values: T[]): T[] {
|
||||
const lastIndex = values.length - 1;
|
||||
const ticks: T[] = [];
|
||||
let lastLabel: string | null = null;
|
||||
for (const idx of indices) {
|
||||
if (labels[idx] === lastLabel) {
|
||||
if (idx === lastIndex && ticks.length > 0) ticks[ticks.length - 1] = values[idx];
|
||||
continue;
|
||||
}
|
||||
lastLabel = labels[idx];
|
||||
ticks.push(values[idx]);
|
||||
}
|
||||
return ticks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicit x-axis tick values: evenly spaced across the plot, including first +
|
||||
* last, bounded by how many fit in `plotWidth` and by the count of distinct
|
||||
* labels (so nothing overlaps or repeats). `plotWidth` excludes the y-axis and
|
||||
* margins. Returns `undefined` until a width is known (first paint).
|
||||
*/
|
||||
export function useXAxisTicks(
|
||||
data: Array<Record<string, any>>,
|
||||
dataKey: string,
|
||||
plotWidth: number | undefined,
|
||||
tickFormatter?: (value: any, index: number) => string
|
||||
): any[] | undefined {
|
||||
return useMemo(() => {
|
||||
if (!data?.length || !plotWidth || plotWidth <= 0) return undefined;
|
||||
|
||||
const n = data.length;
|
||||
const fmt = tickFormatter ?? ((v: any) => String(v));
|
||||
const labels = data.map((d, i) => fmt(d[dataKey], i) ?? "");
|
||||
|
||||
let maxChars = 0;
|
||||
for (const label of labels) {
|
||||
if (label.length > maxChars) maxChars = label.length;
|
||||
}
|
||||
|
||||
// Cap at distinct labels: laying out N evenly-spaced labels (vs one-per-period)
|
||||
// keeps spacing even when the first/last period is partial.
|
||||
const fit = estimateMaxLabels(plotWidth, maxChars);
|
||||
const distinct = new Set(labels).size;
|
||||
const target = Math.min(fit, distinct, n);
|
||||
|
||||
// Evenly spaced on screen, then drop any that repeat the previous label.
|
||||
const values = data.map((d) => d[dataKey]);
|
||||
return dedupeTicksByLabel(selectEvenlySpacedIndices(n, target), labels, values);
|
||||
}, [data, dataKey, plotWidth, tickFormatter]);
|
||||
}
|
||||
Reference in New Issue
Block a user