Skip to content

Commit 1530900

Browse files
committed
WIP adding BigNumber chart type
1 parent d722094 commit 1530900

7 files changed

Lines changed: 420 additions & 55 deletions

File tree

apps/webapp/app/components/metrics/QueryWidget.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { z } from "zod";
55
import { assertNever } from "assert-never";
66
import { TSQLResultsTable } from "../code/TSQLResultsTable";
77
import { QueryResultsChart } from "../code/QueryResultsChart";
8+
import { BigNumberCard } from "../primitives/charts/BigNumberCard";
89
import { Dialog, DialogContent, DialogFooter, DialogHeader } from "../primitives/Dialog";
910
import { Button } from "../primitives/Buttons";
1011
import {
@@ -58,6 +59,14 @@ const chartConfigOptions = {
5859
const ChartConfiguration = z.object({ ...chartConfigOptions });
5960
export type ChartConfiguration = z.infer<typeof ChartConfiguration>;
6061

62+
const bigNumberConfigOptions = {
63+
column: z.string(),
64+
aggregation: AggregationType,
65+
};
66+
67+
const BigNumberConfiguration = z.object({ ...bigNumberConfigOptions });
68+
export type BigNumberConfiguration = z.infer<typeof BigNumberConfiguration>;
69+
6170
export const QueryWidgetConfig = z.discriminatedUnion("type", [
6271
z.object({
6372
type: z.literal("table"),
@@ -75,6 +84,10 @@ export const QueryWidgetConfig = z.discriminatedUnion("type", [
7584
type: z.literal("chart"),
7685
...chartConfigOptions,
7786
}),
87+
z.object({
88+
type: z.literal("bignumber"),
89+
...bigNumberConfigOptions,
90+
}),
7891
]);
7992

8093
export type QueryWidgetConfig = z.infer<typeof QueryWidgetConfig>;
@@ -339,6 +352,31 @@ function QueryWidgetBody({
339352
</>
340353
);
341354
}
355+
case "bignumber": {
356+
return (
357+
<>
358+
<BigNumberCard
359+
rows={data.rows}
360+
columns={data.columns}
361+
config={config}
362+
isLoading={isLoading}
363+
/>
364+
<Dialog open={isFullscreen} onOpenChange={setIsFullscreen}>
365+
<DialogContent fullscreen>
366+
<DialogHeader>{title}</DialogHeader>
367+
<div className="flex h-full min-h-0 w-full flex-1 items-center justify-center pt-4">
368+
<BigNumberCard
369+
rows={data.rows}
370+
columns={data.columns}
371+
config={config}
372+
isLoading={isLoading}
373+
/>
374+
</div>
375+
</DialogContent>
376+
</Dialog>
377+
</>
378+
);
379+
}
342380
default: {
343381
assertNever(type);
344382
}

apps/webapp/app/components/primitives/ClientTabs.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ const ClientTabsContent = React.forwardRef<
190190
ref={ref}
191191
className={cn(
192192
"ring-offset-background focus-visible:ring-ring mt-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
193-
className
193+
className,
194+
"data-[state=inactive]:hidden"
194195
)}
195196
{...props}
196197
/>

apps/webapp/app/components/primitives/charts/BigNumber.tsx

Lines changed: 0 additions & 46 deletions
This file was deleted.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import type { OutputColumnMetadata } from "@internal/tsql";
2+
import { useMemo } from "react";
3+
import type { AggregationType, BigNumberConfiguration } from "~/components/metrics/QueryWidget";
4+
import { Spinner } from "../Spinner";
5+
import { Paragraph } from "../Paragraph";
6+
7+
interface BigNumberCardProps {
8+
rows: Record<string, unknown>[];
9+
columns: OutputColumnMetadata[];
10+
config: BigNumberConfiguration;
11+
isLoading?: boolean;
12+
}
13+
14+
/**
15+
* Extracts numeric values from a specific column across all rows
16+
*/
17+
function extractColumnValues(rows: Record<string, unknown>[], column: string): number[] {
18+
const values: number[] = [];
19+
for (const row of rows) {
20+
const val = row[column];
21+
if (typeof val === "number") {
22+
values.push(val);
23+
} else if (typeof val === "string") {
24+
const parsed = parseFloat(val);
25+
if (!isNaN(parsed)) {
26+
values.push(parsed);
27+
}
28+
}
29+
}
30+
return values;
31+
}
32+
33+
/**
34+
* Aggregate an array of numbers using the specified aggregation function
35+
*/
36+
function aggregateValues(values: number[], aggregation: AggregationType): number {
37+
if (values.length === 0) return 0;
38+
switch (aggregation) {
39+
case "sum":
40+
return values.reduce((a, b) => a + b, 0);
41+
case "avg":
42+
return values.reduce((a, b) => a + b, 0) / values.length;
43+
case "count":
44+
return values.length;
45+
case "min":
46+
return Math.min(...values);
47+
case "max":
48+
return Math.max(...values);
49+
}
50+
}
51+
52+
/**
53+
* Formats a number for display as a big number.
54+
* Uses K/M suffixes for large values, appropriate decimal places for small values.
55+
*/
56+
function formatBigNumber(value: number): { formatted: string; suffix?: string } {
57+
if (Math.abs(value) >= 1_000_000_000) {
58+
const v = value / 1_000_000_000;
59+
return { formatted: v % 1 === 0 ? v.toFixed(0) : v.toFixed(1), suffix: "B" };
60+
}
61+
if (Math.abs(value) >= 1_000_000) {
62+
const v = value / 1_000_000;
63+
return { formatted: v % 1 === 0 ? v.toFixed(0) : v.toFixed(1), suffix: "M" };
64+
}
65+
if (Math.abs(value) >= 1_000) {
66+
const v = value / 1_000;
67+
return { formatted: v % 1 === 0 ? v.toFixed(0) : v.toFixed(1), suffix: "K" };
68+
}
69+
if (Number.isInteger(value)) {
70+
return { formatted: value.toLocaleString() };
71+
}
72+
if (Math.abs(value) < 0.01) {
73+
return { formatted: value.toFixed(4) };
74+
}
75+
if (Math.abs(value) < 1) {
76+
return { formatted: value.toFixed(3) };
77+
}
78+
return { formatted: value.toFixed(2) };
79+
}
80+
81+
export function BigNumberCard({ rows, columns, config, isLoading = false }: BigNumberCardProps) {
82+
const { column, aggregation } = config;
83+
84+
const result = useMemo(() => {
85+
if (rows.length === 0) return null;
86+
87+
const values = extractColumnValues(rows, column);
88+
if (values.length === 0) return null;
89+
90+
return aggregateValues(values, aggregation);
91+
}, [rows, column, aggregation]);
92+
93+
if (isLoading) {
94+
return (
95+
<div className="flex h-full items-center justify-center p-6">
96+
<Spinner className="size-6" />
97+
</div>
98+
);
99+
}
100+
101+
if (result === null) {
102+
return (
103+
<div className="flex h-full items-center justify-center p-6">
104+
<Paragraph variant="small" className="text-text-dimmed">
105+
No data to display
106+
</Paragraph>
107+
</div>
108+
);
109+
}
110+
111+
const { formatted, suffix } = formatBigNumber(result);
112+
113+
return (
114+
<div className="flex h-full items-center justify-center p-6">
115+
<div className="text-[3.75rem] font-normal tabular-nums leading-none text-text-bright">
116+
<div className="flex items-baseline gap-1">
117+
{formatted}
118+
{suffix && <div className="text-2xl text-text-dimmed">{suffix}</div>}
119+
</div>
120+
</div>
121+
</div>
122+
);
123+
}

0 commit comments

Comments
 (0)