Skip to content

Commit f6e3df1

Browse files
committed
Add support for prettyFormat
1 parent a57f5ac commit f6e3df1

File tree

15 files changed

+543
-23
lines changed

15 files changed

+543
-23
lines changed

apps/webapp/app/components/code/QueryResultsChart.tsx

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import type { OutputColumnMetadata } from "@internal/clickhouse";
1+
import type { ColumnFormatType, OutputColumnMetadata } from "@internal/clickhouse";
2+
import { formatDurationMilliseconds } from "@trigger.dev/core/v3";
23
import { BarChart3, LineChart } from "lucide-react";
34
import { memo, useMemo } from "react";
5+
import { createValueFormatter } from "~/utils/columnFormat";
6+
import { formatCurrencyAccurate } from "~/utils/numberFormatter";
47
import type { ChartConfig } from "~/components/primitives/charts/Chart";
58
import { Chart } from "~/components/primitives/charts/ChartCompound";
69
import { ChartBlankState } from "../primitives/charts/ChartBlankState";
@@ -798,8 +801,24 @@ export const QueryResultsChart = memo(function QueryResultsChart({
798801
};
799802
}, [isDateBased, timeGranularity]);
800803

801-
// Create dynamic Y-axis formatter based on data range
802-
const yAxisFormatter = useMemo(() => createYAxisFormatter(data, series), [data, series]);
804+
// Resolve the Y-axis column format for formatting
805+
const yAxisFormat = useMemo(() => {
806+
if (yAxisColumns.length === 0) return undefined;
807+
const col = columns.find((c) => c.name === yAxisColumns[0]);
808+
return (col?.format ?? col?.customRenderType) as ColumnFormatType | undefined;
809+
}, [yAxisColumns, columns]);
810+
811+
// Create dynamic Y-axis formatter based on data range and format
812+
const yAxisFormatter = useMemo(
813+
() => createYAxisFormatter(data, series, yAxisFormat),
814+
[data, series, yAxisFormat]
815+
);
816+
817+
// Create value formatter for tooltips and legend based on column format
818+
const tooltipValueFormatter = useMemo(
819+
() => createValueFormatter(yAxisFormat),
820+
[yAxisFormat]
821+
);
803822

804823
// Check if the group-by column has a runStatus customRenderType
805824
const groupByIsRunStatus = useMemo(() => {
@@ -1019,6 +1038,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
10191038
showLegend={showLegend}
10201039
maxLegendItems={fullLegend ? Infinity : 5}
10211040
legendAggregation={config.aggregation}
1041+
legendValueFormatter={tooltipValueFormatter}
10221042
minHeight="300px"
10231043
fillContainer
10241044
onViewAllLegendItems={onViewAllLegendItems}
@@ -1030,6 +1050,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
10301050
yAxisProps={yAxisProps}
10311051
stackId={stacked ? "stack" : undefined}
10321052
tooltipLabelFormatter={tooltipLabelFormatter}
1053+
tooltipValueFormatter={tooltipValueFormatter}
10331054
/>
10341055
</Chart.Root>
10351056
);
@@ -1046,6 +1067,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
10461067
showLegend={showLegend}
10471068
maxLegendItems={fullLegend ? Infinity : 5}
10481069
legendAggregation={config.aggregation}
1070+
legendValueFormatter={tooltipValueFormatter}
10491071
minHeight="300px"
10501072
fillContainer
10511073
onViewAllLegendItems={onViewAllLegendItems}
@@ -1057,16 +1079,21 @@ export const QueryResultsChart = memo(function QueryResultsChart({
10571079
yAxisProps={yAxisProps}
10581080
stacked={stacked && sortedSeries.length > 1}
10591081
tooltipLabelFormatter={tooltipLabelFormatter}
1082+
tooltipValueFormatter={tooltipValueFormatter}
10601083
lineType="linear"
10611084
/>
10621085
</Chart.Root>
10631086
);
10641087
});
10651088

10661089
/**
1067-
* Creates a Y-axis value formatter based on the data range
1090+
* Creates a Y-axis value formatter based on the data range and optional format hint
10681091
*/
1069-
function createYAxisFormatter(data: Record<string, unknown>[], series: string[]) {
1092+
function createYAxisFormatter(
1093+
data: Record<string, unknown>[],
1094+
series: string[],
1095+
format?: ColumnFormatType
1096+
) {
10701097
// Find min and max values across all series
10711098
let minVal = Infinity;
10721099
let maxVal = -Infinity;
@@ -1083,6 +1110,46 @@ function createYAxisFormatter(data: Record<string, unknown>[], series: string[])
10831110

10841111
const range = maxVal - minVal;
10851112

1113+
// Format-aware formatters
1114+
if (format === "bytes" || format === "decimalBytes") {
1115+
const divisor = format === "bytes" ? 1024 : 1000;
1116+
const units =
1117+
format === "bytes"
1118+
? ["B", "KiB", "MiB", "GiB", "TiB"]
1119+
: ["B", "KB", "MB", "GB", "TB"];
1120+
return (value: number): string => {
1121+
if (value === 0) return "0 B";
1122+
// Use consistent unit for all ticks based on max value
1123+
const i = Math.min(
1124+
Math.floor(Math.log(Math.abs(maxVal || 1)) / Math.log(divisor)),
1125+
units.length - 1
1126+
);
1127+
const scaled = value / Math.pow(divisor, i);
1128+
return `${scaled.toFixed(scaled < 10 ? 1 : 0)} ${units[i]}`;
1129+
};
1130+
}
1131+
1132+
if (format === "percent") {
1133+
return (value: number): string => `${value.toFixed(range < 1 ? 2 : 1)}%`;
1134+
}
1135+
1136+
if (format === "duration") {
1137+
return (value: number): string => formatDurationMilliseconds(value, { style: "short" });
1138+
}
1139+
1140+
if (format === "durationSeconds") {
1141+
return (value: number): string =>
1142+
formatDurationMilliseconds(value * 1000, { style: "short" });
1143+
}
1144+
1145+
if (format === "costInDollars" || format === "cost") {
1146+
return (value: number): string => {
1147+
const dollars = format === "cost" ? value / 100 : value;
1148+
return formatCurrencyAccurate(dollars);
1149+
};
1150+
}
1151+
1152+
// Default formatter
10861153
return (value: number): string => {
10871154
// Use abbreviations for large numbers
10881155
if (Math.abs(value) >= 1_000_000) {

apps/webapp/app/components/code/TSQLResultsTable.tsx

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { useCopy } from "~/hooks/useCopy";
3535
import { useOrganization } from "~/hooks/useOrganizations";
3636
import { useProject } from "~/hooks/useProject";
3737
import { cn } from "~/utils/cn";
38+
import { formatBytes, formatDecimalBytes, formatQuantity } from "~/utils/columnFormat";
3839
import { formatCurrencyAccurate, formatNumber } from "~/utils/numberFormatter";
3940
import { v3ProjectPath, v3RunPathFromFriendlyId } from "~/utils/pathBuilder";
4041
import { ChartBlankState } from "../primitives/charts/ChartBlankState";
@@ -66,9 +67,10 @@ function getFormattedValue(value: unknown, column: OutputColumnMetadata): string
6667
if (value === null) return "NULL";
6768
if (value === undefined) return "";
6869

69-
// Handle custom render types
70-
if (column.customRenderType) {
71-
switch (column.customRenderType) {
70+
// Handle format hints (from prettyFormat() or auto-populated from customRenderType)
71+
const formatType = column.format ?? column.customRenderType;
72+
if (formatType) {
73+
switch (formatType) {
7274
case "duration":
7375
if (typeof value === "number") {
7476
return formatDurationMilliseconds(value, { style: "short" });
@@ -95,6 +97,26 @@ function getFormattedValue(value: unknown, column: OutputColumnMetadata): string
9597
return value;
9698
}
9799
break;
100+
case "bytes":
101+
if (typeof value === "number") {
102+
return formatBytes(value);
103+
}
104+
break;
105+
case "decimalBytes":
106+
if (typeof value === "number") {
107+
return formatDecimalBytes(value);
108+
}
109+
break;
110+
case "percent":
111+
if (typeof value === "number") {
112+
return `${value.toFixed(2)}%`;
113+
}
114+
break;
115+
case "quantity":
116+
if (typeof value === "number") {
117+
return formatQuantity(value);
118+
}
119+
break;
98120
}
99121
}
100122

@@ -222,6 +244,21 @@ function getDisplayLength(value: unknown, column: OutputColumnMetadata): number
222244
if (value === null) return 4; // "NULL"
223245
if (value === undefined) return 9; // "UNDEFINED"
224246

247+
// Handle format hint types - estimate their rendered width
248+
const fmt = column.format;
249+
if (fmt === "bytes" || fmt === "decimalBytes") {
250+
// e.g., "1.50 GiB" or "256.00 MB"
251+
return 12;
252+
}
253+
if (fmt === "percent") {
254+
// e.g., "45.23%"
255+
return 8;
256+
}
257+
if (fmt === "quantity") {
258+
// e.g., "1.50M"
259+
return 8;
260+
}
261+
225262
// Handle custom render types - estimate their rendered width
226263
if (column.customRenderType) {
227264
switch (column.customRenderType) {
@@ -394,6 +431,10 @@ function isRightAlignedColumn(column: OutputColumnMetadata): boolean {
394431
) {
395432
return true;
396433
}
434+
const fmt = column.format;
435+
if (fmt === "bytes" || fmt === "decimalBytes" || fmt === "percent" || fmt === "quantity") {
436+
return true;
437+
}
397438
return isNumericType(column.type);
398439
}
399440

@@ -476,6 +517,32 @@ function CellValue({
476517
return <pre className="text-text-dimmed">UNDEFINED</pre>;
477518
}
478519

520+
// Check format hint for new format types (from prettyFormat())
521+
if (column.format && !column.customRenderType) {
522+
switch (column.format) {
523+
case "bytes":
524+
if (typeof value === "number") {
525+
return <span className="tabular-nums">{formatBytes(value)}</span>;
526+
}
527+
break;
528+
case "decimalBytes":
529+
if (typeof value === "number") {
530+
return <span className="tabular-nums">{formatDecimalBytes(value)}</span>;
531+
}
532+
break;
533+
case "percent":
534+
if (typeof value === "number") {
535+
return <span className="tabular-nums">{value.toFixed(2)}%</span>;
536+
}
537+
break;
538+
case "quantity":
539+
if (typeof value === "number") {
540+
return <span className="tabular-nums">{formatQuantity(value)}</span>;
541+
}
542+
break;
543+
}
544+
}
545+
479546
// First check customRenderType for special rendering
480547
if (column.customRenderType) {
481548
switch (column.customRenderType) {

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import type { OutputColumnMetadata } from "@internal/tsql";
1+
import type { ColumnFormatType, OutputColumnMetadata } from "@internal/tsql";
22
import { Hash } from "lucide-react";
33
import { useMemo } from "react";
44
import type {
55
BigNumberAggregationType,
66
BigNumberConfiguration,
77
} from "~/components/metrics/QueryWidget";
8+
import { createValueFormatter } from "~/utils/columnFormat";
89
import { AnimatedNumber } from "../AnimatedNumber";
910
import { ChartBlankState } from "./ChartBlankState";
1011
import { Spinner } from "../Spinner";
@@ -130,6 +131,15 @@ export function BigNumberCard({ rows, columns, config, isLoading = false }: BigN
130131
return aggregateValues(values, aggregation);
131132
}, [rows, column, aggregation, sortDirection]);
132133

134+
// Look up column format for format-aware display
135+
const columnValueFormatter = useMemo(() => {
136+
const columnMeta = columns.find((c) => c.name === column);
137+
const formatType = (columnMeta?.format ?? columnMeta?.customRenderType) as
138+
| ColumnFormatType
139+
| undefined;
140+
return createValueFormatter(formatType);
141+
}, [columns, column]);
142+
133143
if (isLoading) {
134144
return (
135145
<div className="grid h-full place-items-center [container-type:size]">
@@ -142,14 +152,29 @@ export function BigNumberCard({ rows, columns, config, isLoading = false }: BigN
142152
return <ChartBlankState icon={Hash} message="No data to display" />;
143153
}
144154

155+
// Use format-aware formatter when available
156+
if (columnValueFormatter) {
157+
return (
158+
<div className="h-full w-full [container-type:size]">
159+
<div className="grid h-full w-full place-items-center">
160+
<div className="flex items-baseline gap-[0.15em] whitespace-nowrap text-[clamp(24px,12cqw,96px)] font-normal tabular-nums leading-none text-text-bright">
161+
{prefix && <span>{prefix}</span>}
162+
<span>{columnValueFormatter(result)}</span>
163+
{suffix && <span className="text-[0.4em] text-text-dimmed">{suffix}</span>}
164+
</div>
165+
</div>
166+
</div>
167+
);
168+
}
169+
145170
const { displayValue, unitSuffix, decimalPlaces } = abbreviate
146171
? abbreviateValue(result)
147172
: { displayValue: result, unitSuffix: undefined, decimalPlaces: getDecimalPlaces(result) };
148173

149174
return (
150175
<div className="h-full w-full [container-type:size]">
151176
<div className="grid h-full w-full place-items-center">
152-
<div className="flex items-baseline gap-[0.15em] whitespace-nowrap font-normal tabular-nums leading-none text-text-bright text-[clamp(24px,12cqw,96px)]">
177+
<div className="flex items-baseline gap-[0.15em] whitespace-nowrap text-[clamp(24px,12cqw,96px)] font-normal tabular-nums leading-none text-text-bright">
153178
{prefix && <span>{prefix}</span>}
154179
<AnimatedNumber value={displayValue} decimalPlaces={decimalPlaces} />
155180
{(unitSuffix || suffix) && (

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ const ChartTooltipContent = React.forwardRef<
104104
indicator?: "line" | "dot" | "dashed";
105105
nameKey?: string;
106106
labelKey?: string;
107+
/** Optional formatter for numeric values (e.g. bytes, duration) */
108+
valueFormatter?: (value: number) => string;
107109
}
108110
>(
109111
(
@@ -121,6 +123,7 @@ const ChartTooltipContent = React.forwardRef<
121123
color,
122124
nameKey,
123125
labelKey,
126+
valueFormatter,
124127
},
125128
ref
126129
) => {
@@ -221,9 +224,11 @@ const ChartTooltipContent = React.forwardRef<
221224
{itemConfig?.label || item.name}
222225
</span>
223226
</div>
224-
{item.value && (
227+
{item.value != null && (
225228
<span className="text-foreground font-mono font-medium tabular-nums">
226-
{item.value.toLocaleString()}
229+
{valueFormatter && typeof item.value === "number"
230+
? valueFormatter(item.value)
231+
: item.value.toLocaleString()}
227232
</span>
228233
)}
229234
</div>

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export type ChartBarRendererProps = {
4141
referenceLine?: ReferenceLineProps;
4242
/** Custom tooltip label formatter */
4343
tooltipLabelFormatter?: (label: string, payload: any[]) => string;
44+
/** Optional formatter for numeric tooltip values (e.g. bytes, duration) */
45+
tooltipValueFormatter?: (value: number) => string;
4446
/** Width injected by ResponsiveContainer */
4547
width?: number;
4648
/** Height injected by ResponsiveContainer */
@@ -65,6 +67,7 @@ export function ChartBarRenderer({
6567
yAxisProps: yAxisPropsProp,
6668
referenceLine,
6769
tooltipLabelFormatter,
70+
tooltipValueFormatter,
6871
width,
6972
height,
7073
}: ChartBarRendererProps) {
@@ -163,7 +166,7 @@ export function ChartBarRenderer({
163166
showLegend ? (
164167
() => null
165168
) : tooltipLabelFormatter ? (
166-
<ChartTooltipContent />
169+
<ChartTooltipContent valueFormatter={tooltipValueFormatter} />
167170
) : (
168171
<ZoomTooltip
169172
isSelecting={zoom?.isSelecting}

0 commit comments

Comments
 (0)