all is well
Some checks failed
Build and publish Docker image / build-and-push (push) Failing after 7m0s
CI / Check (push) Failing after 7m9s

This commit is contained in:
Andras Schmelczer 2026-05-17 17:20:19 +01:00
parent eac1bd0d13
commit 2f149503bb
53 changed files with 1543 additions and 354 deletions

View file

@ -30,6 +30,42 @@ function pickTicks(min: number, max: number, count: number): number[] {
return ticks;
}
function isLowCardinalityHistogram(counts: number[], p1: number, p99: number): boolean {
return counts.length > 0 && counts.length <= 10 && p99 > p1 && p99 - p1 <= 10;
}
export function compactHistogramLabel(
index: number,
barCount: number,
p1: number,
p99: number,
center: number,
formatLabel: (value: number) => string,
integerLabels = false
): string {
const formatAxisValue = (value: number) =>
integerLabels ? Math.round(value).toLocaleString() : formatLabel(value);
if (barCount <= 1) return formatAxisValue(center);
const middleBins = barCount - 2;
if (index === 0) {
if (!integerLabels) return `<${formatLabel(p1)}`;
const firstBoundary = Math.ceil(p1);
return firstBoundary <= 1 ? '0' : `<${firstBoundary.toLocaleString()}`;
}
if (index === barCount - 1) {
if (!integerLabels) return `${formatLabel(p99)}+`;
return `${Math.ceil(p99).toLocaleString()}+`;
}
const middleWidth = middleBins > 0 ? (p99 - p1) / middleBins : 0;
if (Math.abs(middleWidth - 1) < 0.001) {
return formatAxisValue(p1 + index - 1);
}
return formatAxisValue(center);
}
export function DualHistogram({
localCounts,
globalCounts,
@ -38,6 +74,8 @@ export function DualHistogram({
globalMean,
meanLabel,
formatLabel,
compact = false,
integerAxisLabels = false,
}: {
localCounts: number[];
globalCounts: number[];
@ -46,9 +84,15 @@ export function DualHistogram({
globalMean?: number;
meanLabel?: string;
formatLabel?: (value: number) => string;
compact?: boolean;
integerAxisLabels?: boolean;
}) {
const { t } = useTranslation();
const targetBars = 25;
const showCompactAxisLabels =
compact &&
isLowCardinalityHistogram(localCounts, p1, p99) &&
isLowCardinalityHistogram(globalCounts, p1, p99);
const targetBars = compact ? (showCompactAxisLabels ? localCounts.length : 16) : 25;
const localBars = downsampleBars(localCounts, targetBars);
const globalBars = downsampleBars(globalCounts, targetBars);
@ -59,6 +103,8 @@ export function DualHistogram({
const fmt =
formatLabel ?? ((v: number) => (Number.isInteger(v) ? v.toLocaleString() : v.toFixed(1)));
if (barCount === 0) return null;
// 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);
@ -97,6 +143,60 @@ export function DualHistogram({
? { right: 0 }
: { left: '50%', transform: 'translateX(-50%)' };
if (compact) {
const axisLabels = showCompactAxisLabels
? barCenters.map((center, index) =>
compactHistogramLabel(index, barCount, p1, p99, center, fmt, integerAxisLabels)
)
: [];
const chartTitle = [
`${fmt(p1)} - ${fmt(p99)}`,
globalMean != null ? `${meanLabel ?? t('areaPane.nationalAvg')}: ${fmt(globalMean)}` : null,
]
.filter(Boolean)
.join('\n');
return (
<div className={showCompactAxisLabels ? 'h-10' : 'h-7'} title={chartTitle}>
<div
className={`${showCompactAxisLabels ? 'h-7' : 'h-full'} relative flex items-end gap-[2px]`}
>
{Array.from({ length: barCount }).map((_, index) => {
const globalHeight = (globalBars[index] / globalMax) * 100;
const localHeight = (localBars[index] / localMax) * 100;
return (
<div key={index} className="relative flex h-full min-w-[2px] flex-1 items-end">
<div
className="absolute bottom-0 left-0 right-0 rounded-t-[2px] bg-warm-300/45 dark:bg-warm-600/50"
style={{ height: `${Math.max(globalHeight, globalBars[index] > 0 ? 8 : 0)}%` }}
/>
{localBars[index] > 0 && (
<div
className="absolute bottom-0 left-[22%] right-[22%] rounded-t-[2px] bg-teal-600 dark:bg-teal-400"
style={{ height: `${Math.max(localHeight, 12)}%` }}
/>
)}
</div>
);
})}
</div>
{showCompactAxisLabels && (
<div className="mt-0.5 flex gap-[2px]">
{axisLabels.map((label, index) => (
<span
key={`${label}-${index}`}
className="min-w-[2px] flex-1 truncate text-center text-[8px] font-medium leading-none text-warm-500 dark:text-warm-400"
title={label}
>
{label}
</span>
))}
</div>
)}
</div>
);
}
return (
<div className="mt-1">
<div className={showMeanMarker ? 'relative pt-5' : 'relative'}>
@ -152,35 +252,29 @@ export function DualHistogram({
function SkeletonHistogram() {
return (
<div className="bg-warm-50 dark:bg-warm-800 rounded p-2 animate-pulse">
<div className="flex justify-between items-baseline">
<div className="h-3 w-24 bg-warm-200 dark:bg-warm-700 rounded" />
<div className="h-3 w-10 bg-warm-200 dark:bg-warm-700 rounded" />
</div>
<div className="flex items-end gap-px h-10 mt-2">
{Array.from({ length: 15 }).map((_, i) => (
<div className="grid min-h-10 animate-pulse grid-cols-[minmax(0,1fr)_6.5rem_minmax(3.5rem,auto)] items-center gap-3 py-1.5">
<div className="h-3 w-24 rounded bg-warm-200 dark:bg-warm-700" />
<div className="flex h-7 items-end gap-[2px]">
{Array.from({ length: 12 }).map((_, i) => (
<div
key={i}
className="flex-1 bg-warm-200 dark:bg-warm-700 rounded-t-sm min-w-[2px]"
style={{ height: `${20 + Math.sin(i * 0.7) * 30 + 30}%` }}
className="min-w-[2px] flex-1 rounded-t-[2px] bg-warm-200 dark:bg-warm-700"
style={{ height: `${22 + Math.sin(i * 0.7) * 28 + 30}%` }}
/>
))}
</div>
<div className="flex justify-between mt-1">
<div className="h-2 w-6 bg-warm-200 dark:bg-warm-700 rounded" />
<div className="h-2 w-6 bg-warm-200 dark:bg-warm-700 rounded" />
</div>
<div className="h-3 w-10 justify-self-end rounded bg-warm-200 dark:bg-warm-700" />
</div>
);
}
export function LoadingSkeleton() {
return (
<div className="p-3 space-y-4">
<div className="space-y-4 p-3">
{[0, 1, 2].map((groupIdx) => (
<div key={groupIdx}>
<div className="h-3 w-20 bg-warm-200 dark:bg-warm-700 rounded animate-pulse mb-2" />
<div className="space-y-3">
<div className="mb-2 h-3 w-20 animate-pulse rounded bg-warm-200 dark:bg-warm-700" />
<div className="divide-y divide-warm-100 dark:divide-navy-800">
{Array.from({ length: groupIdx === 0 ? 3 : 2 }).map((_, i) => (
<SkeletonHistogram key={i} />
))}