all is well
This commit is contained in:
parent
eac1bd0d13
commit
2f149503bb
53 changed files with 1543 additions and 354 deletions
|
|
@ -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} />
|
||||
))}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue