Lots of frontend changes

This commit is contained in:
Andras Schmelczer 2026-02-07 19:10:53 +00:00
parent ec29631c44
commit 555ba7cf53
38 changed files with 1508 additions and 648 deletions

View file

@ -11,18 +11,37 @@ function downsampleBars(counts: number[], targetBars: number): number[] {
return bars;
}
function pickTicks(min: number, max: number, count: number): number[] {
if (max <= min) return [min];
const range = max - min;
const rawStep = range / (count - 1);
const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep)));
const nice = [1, 2, 2.5, 3, 4, 5, 10].find((n) => n * magnitude >= rawStep) ?? 10;
const step = nice * magnitude;
const start = Math.ceil(min / step) * step;
const ticks: number[] = [];
for (let v = start; v <= max + step * 0.01; v += step) {
ticks.push(v);
}
// Ensure at least min and max are represented
if (ticks.length === 0) return [min, max];
return ticks;
}
export function DualHistogram({
localCounts,
globalCounts,
min,
max,
p1,
p99,
globalMean,
formatLabel,
}: {
localCounts: number[];
globalCounts: number[];
min: number;
max: number;
p1: number;
p99: number;
globalMean?: number;
formatLabel?: (value: number) => string;
}) {
const targetBars = 25;
const localBars = downsampleBars(localCounts, targetBars);
@ -32,7 +51,37 @@ export function DualHistogram({
const localMax = Math.max(...localBars, 1);
const globalMax = Math.max(...globalBars, 1);
const meanFraction = globalMean != null && max > min ? (globalMean - min) / (max - min) : null;
const fmt = formatLabel ?? ((v: number) => (Number.isInteger(v) ? v.toLocaleString() : v.toFixed(1)));
// Compute center value for each bar.
// Bar 0 = low outlier, bars 1..n-2 = middle (p1 to p99), bar n-1 = high outlier.
const middleBins = Math.max(barCount - 2, 0);
const middleWidth = middleBins > 0 && p99 > p1 ? (p99 - p1) / middleBins : 0;
const barCenters: number[] = Array.from({ length: barCount }, (_, i) => {
if (i === 0) return p1; // outlier bin, label as p1
if (i === barCount - 1) return p99; // outlier bin, label as p99
return p1 + (i - 1 + 0.5) * middleWidth;
});
// Pick nice tick values and assign each to the nearest bar
const ticks = p99 > p1 ? pickTicks(p1, p99, 6) : [];
const tickBars = new Map<number, string>(); // bar index → label
for (const v of ticks) {
let bestBar = 1;
let bestDist = Infinity;
for (let i = 1; i < barCount - 1; i++) {
const dist = Math.abs(barCenters[i] - v);
if (dist < bestDist) { bestDist = dist; bestBar = i; }
}
if (!tickBars.has(bestBar)) tickBars.set(bestBar, fmt(v));
}
// Mean line: position as fraction across the bar area
const meanFrac = globalMean != null && p99 > p1 ? (globalMean - p1) / (p99 - p1) : null;
// Account for outlier bins: middle region spans bars 1..n-2
const meanPct = meanFrac != null
? ((1 + meanFrac * middleBins) / barCount) * 100
: null;
return (
<div className="mt-1">
@ -56,13 +105,26 @@ export function DualHistogram({
</div>
);
})}
{meanFraction != null && meanFraction >= 0 && meanFraction <= 1 && (
{meanPct != null && meanPct >= 0 && meanPct <= 100 && (
<div
className="absolute bottom-0 top-0 w-px border-l border-dashed border-warm-400 dark:border-warm-500"
style={{ left: `${meanFraction * 100}%` }}
style={{ left: `${meanPct}%` }}
/>
)}
</div>
{tickBars.size > 0 && (
<div className="flex gap-px mt-0.5">
{Array.from({ length: barCount }).map((_, index) => (
<div key={index} className="flex-1 min-w-[2px] text-center">
{tickBars.has(index) && (
<span className="text-[9px] leading-none text-warm-400 dark:text-warm-500">
{tickBars.get(index)}
</span>
)}
</div>
))}
</div>
)}
</div>
);
}