Correct product analytics cohorts and reporting
CI / verify (push) Has been cancelled
CI / publish (push) Has been cancelled

This commit is contained in:
Rocky
2026-08-21 17:50:35 +08:00
parent b25f5ae6e9
commit edd0d9feca
31 changed files with 1134 additions and 262 deletions
@@ -6,8 +6,8 @@ type ChartTone = "primary" | "violet" | "success" | "warning";
export interface ComparisonBarItem {
id?: string;
label: string;
value: number;
secondaryValue?: number;
value: number | null;
secondaryValue?: number | null;
hint?: string;
}
@@ -33,7 +33,7 @@ export function ComparisonBarChart({
}
const maximum = Math.max(
...items.flatMap((item) => [item.value, item.secondaryValue ?? 0]),
...items.flatMap((item) => [item.value ?? 0, item.secondaryValue ?? 0]),
1,
);
const legend = [
@@ -56,15 +56,17 @@ export function ComparisonBarChart({
maximum={maximum}
tone={primaryTone}
value={item.value}
valueLabel={valueFormatter(item.value)}
valueLabel={item.value == null ? "—" : valueFormatter(item.value)}
/>
{secondaryLabel != null && item.secondaryValue != null ? (
{secondaryLabel != null && item.secondaryValue !== undefined ? (
<Bar
label={`${item.label} ${secondaryLabel}`}
maximum={maximum}
tone={secondaryTone}
value={item.secondaryValue}
valueLabel={valueFormatter(item.secondaryValue)}
valueLabel={
item.secondaryValue == null ? "—" : valueFormatter(item.secondaryValue)
}
/>
) : null}
</div>
@@ -84,7 +86,7 @@ function Bar({
label: string;
maximum: number;
tone: ChartTone;
value: number;
value: number | null;
valueLabel: string;
}) {
return (
@@ -92,8 +94,8 @@ function Bar({
<progress
className={`bar-progress bar-progress--${tone} block h-2.5 min-w-0 flex-1 overflow-hidden rounded-full`}
max={maximum}
value={value}
aria-label={`${label}${valueLabel}`}
value={value ?? 0}
aria-label={value == null ? `${label}:暂无数据` : `${label}${valueLabel}`}
/>
<strong className="w-16 shrink-0 text-right text-xs font-semibold tabular-nums text-foreground">
{valueLabel}
@@ -12,7 +12,7 @@ export function FunnelChart({
return <p className="p-8 text-center text-sm text-muted">{emptyText}</p>;
}
const maximum = Math.max(steps[0]?.count ?? 0, ...steps.map((step) => step.count), 1);
const maximum = Math.max(steps[0]?.count ?? 0, 1);
return (
<div className="space-y-5 p-5 sm:p-6">
@@ -7,11 +7,11 @@ export function RadialMetric({
tone = "primary",
}: {
label: string;
percent: number;
percent: number | null;
detail?: string;
tone?: RadialTone;
}) {
const value = Math.min(Math.max(percent, 0), 100);
const value = percent == null ? null : Math.min(Math.max(percent, 0), 100);
return (
<div className="flex items-center gap-5">
@@ -19,7 +19,7 @@ export function RadialMetric({
className="size-28 shrink-0"
viewBox="0 0 120 120"
role="img"
aria-label={`${label}${value.toFixed(1)}%`}
aria-label={value == null ? `${label}:暂无数据` : `${label}${value.toFixed(1)}%`}
>
<circle className="radial-track" cx="60" cy="60" r="48" pathLength="100" />
<circle
@@ -28,11 +28,11 @@ export function RadialMetric({
cy="60"
r="48"
pathLength="100"
strokeDasharray={`${value} ${100 - value}`}
strokeDasharray={`${value ?? 0} ${100 - (value ?? 0)}`}
transform="rotate(-90 60 60)"
/>
<text className="radial-label" x="60" y="65" textAnchor="middle">
{Math.round(value)}%
{value == null ? "—" : `${Math.round(value)}%`}
</text>
</svg>
<div>
@@ -0,0 +1,20 @@
export function PeriodCaption({
from,
until,
}: {
from: string;
until: string;
}) {
return (
<p className="text-xs text-muted" aria-label="实际统计周期">
{utcLabel(from)} {utcLabel(until)}UTC
</p>
);
}
function utcLabel(value: string): string {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "—";
return date.toISOString().replace("T", " ").slice(0, 16);
}