Correct product analytics cohorts and reporting
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user