Skip to content

Commit e0ea070

Browse files
committed
Improved BigNumber config and styling
1 parent 1530900 commit e0ea070

4 files changed

Lines changed: 159 additions & 25 deletions

File tree

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,26 @@ const chartConfigOptions = {
5959
const ChartConfiguration = z.object({ ...chartConfigOptions });
6060
export type ChartConfiguration = z.infer<typeof ChartConfiguration>;
6161

62+
const BigNumberAggregationType = z.union([
63+
z.literal("sum"),
64+
z.literal("avg"),
65+
z.literal("count"),
66+
z.literal("min"),
67+
z.literal("max"),
68+
z.literal("first"),
69+
z.literal("last"),
70+
]);
71+
export type BigNumberAggregationType = z.infer<typeof BigNumberAggregationType>;
72+
73+
const BigNumberSortDirection = z.union([z.literal("asc"), z.literal("desc")]);
74+
6275
const bigNumberConfigOptions = {
6376
column: z.string(),
64-
aggregation: AggregationType,
77+
aggregation: BigNumberAggregationType,
78+
sortDirection: BigNumberSortDirection.optional(),
79+
abbreviate: z.boolean().default(false),
80+
prefix: z.string().optional(),
81+
suffix: z.string().optional(),
6582
};
6683

6784
const BigNumberConfiguration = z.object({ ...bigNumberConfigOptions });

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

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { OutputColumnMetadata } from "@internal/tsql";
22
import { useMemo } from "react";
3-
import type { AggregationType, BigNumberConfiguration } from "~/components/metrics/QueryWidget";
3+
import type {
4+
BigNumberAggregationType,
5+
BigNumberConfiguration,
6+
} from "~/components/metrics/QueryWidget";
47
import { Spinner } from "../Spinner";
58
import { Paragraph } from "../Paragraph";
69

@@ -12,11 +15,24 @@ interface BigNumberCardProps {
1215
}
1316

1417
/**
15-
* Extracts numeric values from a specific column across all rows
18+
* Extracts numeric values from a specific column across all rows,
19+
* optionally sorting them first.
1620
*/
17-
function extractColumnValues(rows: Record<string, unknown>[], column: string): number[] {
21+
function extractColumnValues(
22+
rows: Record<string, unknown>[],
23+
column: string,
24+
sortDirection?: "asc" | "desc"
25+
): number[] {
1826
const values: number[] = [];
19-
for (const row of rows) {
27+
const sortedRows = sortDirection
28+
? [...rows].sort((a, b) => {
29+
const aVal = toNumber(a[column]);
30+
const bVal = toNumber(b[column]);
31+
return sortDirection === "asc" ? aVal - bVal : bVal - aVal;
32+
})
33+
: rows;
34+
35+
for (const row of sortedRows) {
2036
const val = row[column];
2137
if (typeof val === "number") {
2238
values.push(val);
@@ -30,10 +46,19 @@ function extractColumnValues(rows: Record<string, unknown>[], column: string): n
3046
return values;
3147
}
3248

49+
function toNumber(value: unknown): number {
50+
if (typeof value === "number") return value;
51+
if (typeof value === "string") {
52+
const parsed = parseFloat(value);
53+
return isNaN(parsed) ? 0 : parsed;
54+
}
55+
return 0;
56+
}
57+
3358
/**
3459
* Aggregate an array of numbers using the specified aggregation function
3560
*/
36-
function aggregateValues(values: number[], aggregation: AggregationType): number {
61+
function aggregateValues(values: number[], aggregation: BigNumberAggregationType): number {
3762
if (values.length === 0) return 0;
3863
switch (aggregation) {
3964
case "sum":
@@ -46,49 +71,59 @@ function aggregateValues(values: number[], aggregation: AggregationType): number
4671
return Math.min(...values);
4772
case "max":
4873
return Math.max(...values);
74+
case "first":
75+
return values[0];
76+
case "last":
77+
return values[values.length - 1];
4978
}
5079
}
5180

5281
/**
53-
* Formats a number for display as a big number.
54-
* Uses K/M suffixes for large values, appropriate decimal places for small values.
82+
* Formats a number for display as a big number with abbreviation (K/M/B suffixes).
5583
*/
56-
function formatBigNumber(value: number): { formatted: string; suffix?: string } {
84+
function formatBigNumberAbbreviated(value: number): { formatted: string; unitSuffix?: string } {
5785
if (Math.abs(value) >= 1_000_000_000) {
5886
const v = value / 1_000_000_000;
59-
return { formatted: v % 1 === 0 ? v.toFixed(0) : v.toFixed(1), suffix: "B" };
87+
return { formatted: v % 1 === 0 ? v.toFixed(0) : v.toFixed(1), unitSuffix: "B" };
6088
}
6189
if (Math.abs(value) >= 1_000_000) {
6290
const v = value / 1_000_000;
63-
return { formatted: v % 1 === 0 ? v.toFixed(0) : v.toFixed(1), suffix: "M" };
91+
return { formatted: v % 1 === 0 ? v.toFixed(0) : v.toFixed(1), unitSuffix: "M" };
6492
}
6593
if (Math.abs(value) >= 1_000) {
6694
const v = value / 1_000;
67-
return { formatted: v % 1 === 0 ? v.toFixed(0) : v.toFixed(1), suffix: "K" };
95+
return { formatted: v % 1 === 0 ? v.toFixed(0) : v.toFixed(1), unitSuffix: "K" };
6896
}
97+
return { formatted: formatPlainNumber(value) };
98+
}
99+
100+
/**
101+
* Formats a number for display without abbreviation.
102+
*/
103+
function formatPlainNumber(value: number): string {
69104
if (Number.isInteger(value)) {
70-
return { formatted: value.toLocaleString() };
105+
return value.toLocaleString();
71106
}
72107
if (Math.abs(value) < 0.01) {
73-
return { formatted: value.toFixed(4) };
108+
return value.toFixed(4);
74109
}
75110
if (Math.abs(value) < 1) {
76-
return { formatted: value.toFixed(3) };
111+
return value.toFixed(3);
77112
}
78-
return { formatted: value.toFixed(2) };
113+
return value.toFixed(2);
79114
}
80115

81116
export function BigNumberCard({ rows, columns, config, isLoading = false }: BigNumberCardProps) {
82-
const { column, aggregation } = config;
117+
const { column, aggregation, sortDirection, abbreviate = false, prefix, suffix } = config;
83118

84119
const result = useMemo(() => {
85120
if (rows.length === 0) return null;
86121

87-
const values = extractColumnValues(rows, column);
122+
const values = extractColumnValues(rows, column, sortDirection);
88123
if (values.length === 0) return null;
89124

90125
return aggregateValues(values, aggregation);
91-
}, [rows, column, aggregation]);
126+
}, [rows, column, aggregation, sortDirection]);
92127

93128
if (isLoading) {
94129
return (
@@ -108,14 +143,23 @@ export function BigNumberCard({ rows, columns, config, isLoading = false }: BigN
108143
);
109144
}
110145

111-
const { formatted, suffix } = formatBigNumber(result);
146+
const { formatted, unitSuffix } = abbreviate
147+
? formatBigNumberAbbreviated(result)
148+
: { formatted: formatPlainNumber(result), unitSuffix: undefined };
112149

113150
return (
114151
<div className="flex h-full items-center justify-center p-6">
115152
<div className="text-[3.75rem] font-normal tabular-nums leading-none text-text-bright">
116153
<div className="flex items-baseline gap-1">
154+
{prefix && <span>{prefix}</span>}
117155
{formatted}
118-
{suffix && <div className="text-2xl text-text-dimmed">{suffix}</div>}
156+
{(unitSuffix || suffix) && (
157+
<div className="text-2xl text-text-dimmed">
158+
{unitSuffix}
159+
{unitSuffix && suffix ? " " : ""}
160+
{suffix}
161+
</div>
162+
)}
119163
</div>
120164
</div>
121165
</div>

apps/webapp/app/components/query/QueryEditor.tsx

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ export function QueryEditor({
404404
initialChartConfig ?? defaultChartConfig
405405
);
406406
const [bigNumberConfig, setBigNumberConfig] = useState<BigNumberConfiguration>(
407-
initialBigNumberConfig ?? { column: "", aggregation: "sum" }
407+
initialBigNumberConfig ?? { column: "", aggregation: "sum", abbreviate: true }
408408
);
409409
const [sidebarTab, setSidebarTab] = useState<string>("ai");
410410
const [aiFixRequest, setAiFixRequest] = useState<{ prompt: string; key: number } | null>(null);
@@ -1270,12 +1270,20 @@ function ResultsBigNumber({
12701270
);
12711271
}
12721272

1273-
const aggregationOptions = [
1273+
const bigNumberAggregationOptions = [
12741274
{ value: "sum", label: "Sum" },
12751275
{ value: "avg", label: "Average" },
12761276
{ value: "count", label: "Count" },
12771277
{ value: "min", label: "Min" },
12781278
{ value: "max", label: "Max" },
1279+
{ value: "first", label: "First" },
1280+
{ value: "last", label: "Last" },
1281+
] as const;
1282+
1283+
const bigNumberSortOptions = [
1284+
{ value: "", label: "Unsorted" },
1285+
{ value: "asc", label: "Ascending" },
1286+
{ value: "desc", label: "Descending" },
12791287
] as const;
12801288

12811289
function BigNumberConfigPanel({
@@ -1321,6 +1329,34 @@ function BigNumberConfigPanel({
13211329
}
13221330
</Select>
13231331
</div>
1332+
<div className="flex flex-col gap-1">
1333+
<Paragraph variant="extra-small" className="text-text-dimmed">
1334+
Sort order
1335+
</Paragraph>
1336+
<Select
1337+
value={config.sortDirection ?? ""}
1338+
setValue={(value) =>
1339+
onChange({
1340+
...config,
1341+
sortDirection: value === "" ? undefined : (value as "asc" | "desc"),
1342+
})
1343+
}
1344+
variant="tertiary/small"
1345+
dropdownIcon={true}
1346+
items={[...bigNumberSortOptions]}
1347+
text={(value) =>
1348+
bigNumberSortOptions.find((o) => o.value === value)?.label ?? "Unsorted"
1349+
}
1350+
>
1351+
{(items) =>
1352+
items.map((item) => (
1353+
<SelectItem key={item.value} value={item.value}>
1354+
{item.label}
1355+
</SelectItem>
1356+
))
1357+
}
1358+
</Select>
1359+
</div>
13241360
<div className="flex flex-col gap-1">
13251361
<Paragraph variant="extra-small" className="text-text-dimmed">
13261362
Aggregation
@@ -1332,8 +1368,10 @@ function BigNumberConfigPanel({
13321368
}
13331369
variant="tertiary/small"
13341370
dropdownIcon={true}
1335-
items={[...aggregationOptions]}
1336-
text={(value) => aggregationOptions.find((o) => o.value === value)?.label ?? value}
1371+
items={[...bigNumberAggregationOptions]}
1372+
text={(value) =>
1373+
bigNumberAggregationOptions.find((o) => o.value === value)?.label ?? value
1374+
}
13371375
>
13381376
{(items) =>
13391377
items.map((item) => (
@@ -1344,6 +1382,37 @@ function BigNumberConfigPanel({
13441382
}
13451383
</Select>
13461384
</div>
1385+
<div className="flex items-center justify-between">
1386+
<Switch
1387+
label="Abbreviate large values"
1388+
labelPosition="right"
1389+
variant="small"
1390+
checked={config.abbreviate ?? false}
1391+
onCheckedChange={(checked) => onChange({ ...config, abbreviate: checked })}
1392+
/>
1393+
</div>
1394+
<div className="flex flex-col gap-1">
1395+
<Paragraph variant="extra-small" className="text-text-dimmed">
1396+
Prefix
1397+
</Paragraph>
1398+
<Input
1399+
value={config.prefix ?? ""}
1400+
onChange={(e) => onChange({ ...config, prefix: e.target.value || undefined })}
1401+
placeholder="e.g. $"
1402+
variant="small"
1403+
/>
1404+
</div>
1405+
<div className="flex flex-col gap-1">
1406+
<Paragraph variant="extra-small" className="text-text-dimmed">
1407+
Suffix
1408+
</Paragraph>
1409+
<Input
1410+
value={config.suffix ?? ""}
1411+
onChange={(e) => onChange({ ...config, suffix: e.target.value || undefined })}
1412+
placeholder="e.g. ms"
1413+
variant="small"
1414+
/>
1415+
</div>
13471416
</div>
13481417
</div>
13491418
);

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.metrics.custom.$dashboardId/route.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,10 @@ export default function Page() {
323323
? {
324324
column: state.editorMode.widget.display.column,
325325
aggregation: state.editorMode.widget.display.aggregation,
326+
sortDirection: state.editorMode.widget.display.sortDirection,
327+
abbreviate: state.editorMode.widget.display.abbreviate,
328+
prefix: state.editorMode.widget.display.prefix,
329+
suffix: state.editorMode.widget.display.suffix,
326330
}
327331
: undefined;
328332
const editorDefaultResultsView =

0 commit comments

Comments
 (0)