Skip to content

Commit a5201d9

Browse files
feat(web): rebuild analytics charts on bklit-ui and fix avg/day (#1093)
* feat(web): rebuild analytics charts on bklit-ui and fix avg/day Fix the home + analytics "avg/day" stat, which divided the all-time total by a fixed 30-day window (~1667); it now divides by the actual tracked span so both pages agree (~239/day). Remove the pre-Convex "legacy" archive callout from the analytics header. Migrate every analytics chart from evilcharts to bklit-ui (shadcn @bklit registry, composable visx + motion) behind two thin wrappers (TrendAreaChart, CategoryBarChart). Charts are themed through the --chart-* CSS vars mapped onto site tokens (primary / border / muted-foreground / popover) for a minimal terminal look that adapts to light and dark automatically. Horizontal bars get a configurable left gutter so category names stay readable. Also fixes a vendored shimmering-text import path and adds @types/d3-array. Removes the evilcharts library and evil-chart-utils. * fix(web): tighten analytics chart layout and remove padding gaps - denser horizontal preference bars (30px/row instead of 38) - drop leftover min-h wrappers that padded empty space below short charts - lay CLI versions in a single 4-column row instead of a 2x2 wall * fix(web): give each area chart a unique reveal clip id bklit's AreaChart hardcoded clipPathId="chart-area-grow-clip", so the two area charts on the analytics page (metrics sparkline + daily timeline) emitted duplicate document-scoped SVG ids and could resolve each other's reveal clip. Derive a per-instance id from useId, matching how the rest of the chart library already generates ids.
1 parent 10b6d55 commit a5201d9

94 files changed

Lines changed: 10368 additions & 6183 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/web/components.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@
2323
"registries": {
2424
"@kibo-ui": "https://www.kibo-ui.com/r/{name}.json",
2525
"@magicui": "https://magicui.design/r/{name}.json",
26-
"@evilcharts": "https://evilcharts.com/r/{name}.json"
26+
"@bklit": "https://bklit.com/r/{name}.json"
2727
}
2828
}

apps/web/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,22 @@
2020
"@number-flow/react": "^0.6.0",
2121
"@orama/orama": "^3.1.18",
2222
"@shikijs/transformers": "^4.0.2",
23+
"@visx/curve": "4.0.1-alpha.0",
24+
"@visx/event": "4.0.1-alpha.0",
25+
"@visx/gradient": "4.0.1-alpha.0",
26+
"@visx/grid": "4.0.1-alpha.0",
27+
"@visx/pattern": "4.0.1-alpha.0",
28+
"@visx/responsive": "4.0.1-alpha.0",
29+
"@visx/scale": "4.0.1-alpha.0",
30+
"@visx/shape": "4.0.1-alpha.0",
2331
"babel-plugin-react-compiler": "^1.0.0",
2432
"class-variance-authority": "^0.7.1",
2533
"clsx": "^2.1.1",
2634
"convex": "catalog:",
2735
"convex-helpers": "catalog:",
2836
"create-better-t-stack": "workspace:*",
2937
"culori": "^4.0.2",
38+
"d3-array": "^3.2.4",
3039
"date-fns": "^4.1.0",
3140
"fumadocs-core": "16.8.10",
3241
"fumadocs-mdx": "15.0.4",
@@ -55,6 +64,7 @@
5564
"devDependencies": {
5665
"@tailwindcss/postcss": "^4.3.0",
5766
"@types/culori": "^4.0.1",
67+
"@types/d3-array": "^3.2.2",
5868
"@types/mdx": "^2.0.13",
5969
"@types/node": "catalog:",
6070
"@types/papaparse": "^5.5.2",

apps/web/src/app/(home)/_components/stats-section.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,19 @@ type GithubRepoStats = {
1313
contributorCount: number;
1414
};
1515

16+
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
17+
18+
function getDaySpan(firstDate: string | null, lastDate: string | null): number {
19+
if (!firstDate || !lastDate) return 0;
20+
const start = Date.parse(`${firstDate}T00:00:00Z`);
21+
const end = Date.parse(`${lastDate}T00:00:00Z`);
22+
if (Number.isNaN(start) || Number.isNaN(end) || end < start) return 0;
23+
return Math.floor((end - start) / MILLISECONDS_PER_DAY) + 1;
24+
}
25+
1626
export default function StatsSection() {
1727
const stats = useQuery(api.analytics.getStats, {});
18-
const dailyStats = useQuery(api.analytics.getDailyStats, { days: 30 });
28+
const monthlyStats = useQuery(api.analytics.getMonthlyStats, {});
1929
const githubRepo = useQuery(api.stats.getGithubRepo, {
2030
name: "AmanVarshney01/create-better-t-stack",
2131
}) as GithubRepoStats | null | undefined;
@@ -26,8 +36,8 @@ export default function StatsSection() {
2636
const liveNpmDownloadCount = useNpmDownloadCounter(npmPackages);
2737

2838
const totalProjects = stats?.totalProjects ?? 0;
29-
const avgProjectsPerDay =
30-
dailyStats && dailyStats.length > 0 ? (totalProjects / dailyStats.length).toFixed(2) : "0";
39+
const trackingDays = getDaySpan(monthlyStats?.firstDate ?? null, monthlyStats?.lastDate ?? null);
40+
const avgProjectsPerDay = trackingDays > 0 ? (totalProjects / trackingDays).toFixed(1) : "0";
3141
const lastUpdated = stats?.lastEventTime
3242
? new Date(stats.lastEventTime).toLocaleDateString("en-US", {
3343
month: "short",

apps/web/src/app/(home)/analytics/_components/analytics-header.tsx

Lines changed: 4 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,10 @@ const utcDateTimeFormatter = new Intl.DateTimeFormat("en-US", {
1414
timeZone: "UTC",
1515
});
1616

17-
const utcDateFormatter = new Intl.DateTimeFormat("en-US", {
18-
month: "short",
19-
day: "numeric",
20-
year: "numeric",
21-
timeZone: "UTC",
22-
});
23-
2417
function formatUtcDateTime(value: string) {
2518
return `${utcDateTimeFormatter.format(new Date(value))} UTC`;
2619
}
2720

28-
function formatUtcDate(value: string) {
29-
return utcDateFormatter.format(new Date(value));
30-
}
31-
3221
function HeaderStat({
3322
label,
3423
value,
@@ -53,22 +42,14 @@ export function AnalyticsHeader({
5342
lastUpdated,
5443
liveTotal,
5544
trackingDays,
56-
legacy,
5745
connectionStatus,
5846
}: {
5947
lastUpdated: string | null;
6048
liveTotal: number;
6149
trackingDays: number;
62-
legacy: {
63-
total: number;
64-
avgPerDay: number;
65-
lastUpdatedIso: string;
66-
source: string;
67-
};
6850
connectionStatus: "online" | "connecting" | "reconnecting" | "offline";
6951
}) {
7052
const formattedDate = lastUpdated ? formatUtcDateTime(lastUpdated) : null;
71-
const legacyDate = formatUtcDate(legacy.lastUpdatedIso);
7253
const statusMeta = {
7354
online: {
7455
label: "Streaming",
@@ -112,7 +93,7 @@ export function AnalyticsHeader({
11293
</div>
11394
</div>
11495

115-
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
96+
<div className="grid gap-4 md:grid-cols-2">
11697
<HeaderStat
11798
label="Live projects"
11899
value={formatCompactNumber(liveTotal)}
@@ -123,35 +104,17 @@ export function AnalyticsHeader({
123104
value={trackingDays}
124105
detail="Calendar days represented in the live telemetry dataset."
125106
/>
126-
<HeaderStat
127-
label="Archive total"
128-
value={formatCompactNumber(legacy.total)}
129-
detail={`Historical creations from the pre-Convex archive through ${legacyDate}.`}
130-
/>
131-
<HeaderStat
132-
label="Archive pace"
133-
value={legacy.avgPerDay.toFixed(1)}
134-
detail="Average project creations per day in the historical snapshot."
135-
/>
136107
</div>
137108

138109
<div className="rounded border border-border bg-fd-background p-4">
139-
<div className="grid gap-3 text-sm md:grid-cols-4">
110+
<div className="flex flex-wrap items-center justify-between gap-3 text-sm">
140111
<div className="flex items-center gap-2">
141112
<DatabaseZap className="h-4 w-4 text-primary" />
142113
<span className="font-mono text-xs text-muted-foreground uppercase">Telemetry</span>
143114
</div>
144-
<div className="flex items-center justify-between gap-3 md:block">
115+
<div className="flex items-center gap-2">
145116
<span className="text-muted-foreground">Latest event</span>
146-
<div className="font-medium md:mt-1">{formattedDate ?? "Waiting"}</div>
147-
</div>
148-
<div className="flex items-center justify-between gap-3 md:block">
149-
<span className="text-muted-foreground">Archive snapshot</span>
150-
<div className="font-medium md:mt-1">{legacyDate}</div>
151-
</div>
152-
<div className="flex items-center justify-between gap-3 md:block">
153-
<span className="text-muted-foreground">Source</span>
154-
<div className="font-medium md:mt-1">{legacy.source}</div>
117+
<span className="font-medium">{formattedDate ?? "Waiting"}</span>
155118
</div>
156119
</div>
157120
</div>

apps/web/src/app/(home)/analytics/_components/analytics-page.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,9 @@ import type { AggregatedAnalyticsData } from "./types";
1212

1313
export default function AnalyticsPage({
1414
data,
15-
legacy,
1615
connectionStatus,
1716
}: {
1817
data: AggregatedAnalyticsData;
19-
legacy: {
20-
total: number;
21-
avgPerDay: number;
22-
lastUpdatedIso: string;
23-
source: string;
24-
};
2518
connectionStatus: "online" | "connecting" | "reconnecting" | "offline";
2619
}) {
2720
return (
@@ -31,7 +24,6 @@ export default function AnalyticsPage({
3124
lastUpdated={data.lastUpdated}
3225
liveTotal={data.totalProjects}
3326
trackingDays={data.momentum.trackingDays}
34-
legacy={legacy}
3527
connectionStatus={connectionStatus}
3628
/>
3729

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"use client";
2+
3+
import { curveMonotoneX } from "@visx/curve";
4+
5+
import { Area } from "@/components/charts/area";
6+
import { AreaChart } from "@/components/charts/area-chart";
7+
import { Bar } from "@/components/charts/bar";
8+
import { BarChart } from "@/components/charts/bar-chart";
9+
import { BarXAxis } from "@/components/charts/bar-x-axis";
10+
import { BarYAxis } from "@/components/charts/bar-y-axis";
11+
import { Grid } from "@/components/charts/grid";
12+
import { ChartTooltip, type TooltipRow } from "@/components/charts/tooltip";
13+
import { XAxis } from "@/components/charts/x-axis";
14+
15+
const ACCENT = "var(--chart-line-primary)";
16+
const MUTED = "var(--chart-line-secondary)";
17+
18+
const compactNumber = new Intl.NumberFormat("en", {
19+
notation: "compact",
20+
maximumFractionDigits: 1,
21+
}).format;
22+
23+
export type ChartSeries = {
24+
/** Numeric key in each data row. */
25+
key: string;
26+
/** Human label shown in the tooltip / legend. */
27+
label: string;
28+
/** Series color. Defaults to the site accent (muted for line series). */
29+
color?: string;
30+
/** Render as a stroke-only line instead of a filled area (area chart only). */
31+
line?: boolean;
32+
};
33+
34+
type Row = Record<string, unknown>;
35+
36+
const num = (value: unknown) => Number(value ?? 0);
37+
38+
/**
39+
* Time-series area chart (x is a `Date`). Filled series plus optional overlay
40+
* lines. Styling comes from the shared `--chart-*` theme tokens.
41+
*/
42+
export function TrendAreaChart({
43+
data,
44+
series,
45+
xKey = "date",
46+
height = 300,
47+
valueFormat = compactNumber,
48+
}: {
49+
data: Row[];
50+
series: ChartSeries[];
51+
xKey?: string;
52+
height?: number;
53+
valueFormat?: (value: number) => string;
54+
}) {
55+
const rows = (point: Row): TooltipRow[] =>
56+
series.map((s) => ({
57+
color: s.color ?? (s.line ? MUTED : ACCENT),
58+
label: s.label,
59+
value: valueFormat(num(point[s.key])),
60+
}));
61+
62+
return (
63+
<div className="min-w-0" style={{ height }}>
64+
<AreaChart
65+
data={data}
66+
xDataKey={xKey}
67+
aspectRatio="auto"
68+
className="h-full w-full"
69+
margin={{ top: 16, right: 20, bottom: 28, left: 20 }}
70+
animationDuration={700}
71+
>
72+
<Grid horizontal strokeDasharray="4,4" />
73+
{series.map((s) => {
74+
const color = s.color ?? (s.line ? MUTED : ACCENT);
75+
return (
76+
<Area
77+
key={s.key}
78+
dataKey={s.key}
79+
fill={color}
80+
stroke={color}
81+
fillOpacity={s.line ? 0 : 0.15}
82+
strokeWidth={s.line ? 1.5 : 2}
83+
curve={curveMonotoneX}
84+
showHighlight={false}
85+
/>
86+
);
87+
})}
88+
<XAxis numTicks={5} />
89+
<ChartTooltip rows={rows} />
90+
</AreaChart>
91+
</div>
92+
);
93+
}
94+
95+
/**
96+
* Categorical bar chart. `orientation="horizontal"` puts categories on the Y
97+
* axis (ranked lists); `"vertical"` puts them on the X axis (time buckets).
98+
*/
99+
export function CategoryBarChart({
100+
data,
101+
xKey,
102+
series,
103+
orientation = "vertical",
104+
height = 300,
105+
maxLabels = 12,
106+
labelWidth = 120,
107+
valueFormat = compactNumber,
108+
tooltipLabelKey,
109+
tooltipValueKey,
110+
}: {
111+
data: Row[];
112+
xKey: string;
113+
series: ChartSeries[];
114+
orientation?: "vertical" | "horizontal";
115+
height?: number;
116+
maxLabels?: number;
117+
/** Left gutter (px) reserved for category names when `orientation="horizontal"`. */
118+
labelWidth?: number;
119+
valueFormat?: (value: number) => string;
120+
/** Row field holding the full display name for the tooltip (defaults to `xKey`). */
121+
tooltipLabelKey?: string;
122+
/** Row field holding a preformatted value string (e.g. "1,234 (12%)"). */
123+
tooltipValueKey?: string;
124+
}) {
125+
const horizontal = orientation === "horizontal";
126+
127+
const rows = (point: Row): TooltipRow[] => {
128+
if (series.length === 1) {
129+
const s = series[0];
130+
return [
131+
{
132+
color: s.color ?? ACCENT,
133+
label: String(point[tooltipLabelKey ?? xKey] ?? s.label),
134+
value: tooltipValueKey
135+
? String(point[tooltipValueKey] ?? "")
136+
: valueFormat(num(point[s.key])),
137+
},
138+
];
139+
}
140+
return series.map((s) => ({
141+
color: s.color ?? ACCENT,
142+
label: s.label,
143+
value: valueFormat(num(point[s.key])),
144+
}));
145+
};
146+
147+
return (
148+
<div className="min-w-0" style={{ height }}>
149+
<BarChart
150+
data={data}
151+
xDataKey={xKey}
152+
orientation={orientation}
153+
aspectRatio="auto"
154+
className="h-full w-full"
155+
margin={
156+
horizontal
157+
? { top: 8, right: 16, bottom: 8, left: labelWidth }
158+
: { top: 16, right: 12, bottom: 28, left: 12 }
159+
}
160+
animationDuration={700}
161+
>
162+
<Grid horizontal={!horizontal} vertical={horizontal} strokeDasharray="4,4" />
163+
{series.map((s) => (
164+
<Bar key={s.key} dataKey={s.key} fill={s.color ?? ACCENT} lineCap={4} />
165+
))}
166+
{horizontal ? <BarYAxis /> : <BarXAxis maxLabels={maxLabels} />}
167+
<ChartTooltip showDatePill={false} showCrosshair={false} rows={rows} />
168+
</BarChart>
169+
</div>
170+
);
171+
}

0 commit comments

Comments
 (0)