Skip to content

Commit bf178b2

Browse files
committed
Add support for prettyFormat
1 parent 56b196e commit bf178b2

File tree

15 files changed

+542
-22
lines changed

15 files changed

+542
-22
lines changed

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

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
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 { memo, useMemo } from "react";
4+
import { createValueFormatter } from "~/utils/columnFormat";
5+
import { formatCurrencyAccurate } from "~/utils/numberFormatter";
36
import type { ChartConfig } from "~/components/primitives/charts/Chart";
47
import { Chart } from "~/components/primitives/charts/ChartCompound";
58
import { Paragraph } from "../primitives/Paragraph";
@@ -797,8 +800,24 @@ export const QueryResultsChart = memo(function QueryResultsChart({
797800
};
798801
}, [isDateBased, timeGranularity]);
799802

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

803822
// Check if the group-by column has a runStatus customRenderType
804823
const groupByIsRunStatus = useMemo(() => {
@@ -1016,6 +1035,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
10161035
showLegend={showLegend}
10171036
maxLegendItems={fullLegend ? Infinity : 5}
10181037
legendAggregation={config.aggregation}
1038+
legendValueFormatter={tooltipValueFormatter}
10191039
minHeight="300px"
10201040
fillContainer
10211041
onViewAllLegendItems={onViewAllLegendItems}
@@ -1027,6 +1047,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
10271047
yAxisProps={yAxisProps}
10281048
stackId={stacked ? "stack" : undefined}
10291049
tooltipLabelFormatter={tooltipLabelFormatter}
1050+
tooltipValueFormatter={tooltipValueFormatter}
10301051
/>
10311052
</Chart.Root>
10321053
);
@@ -1043,6 +1064,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
10431064
showLegend={showLegend}
10441065
maxLegendItems={fullLegend ? Infinity : 5}
10451066
legendAggregation={config.aggregation}
1067+
legendValueFormatter={tooltipValueFormatter}
10461068
minHeight="300px"
10471069
fillContainer
10481070
onViewAllLegendItems={onViewAllLegendItems}
@@ -1054,16 +1076,21 @@ export const QueryResultsChart = memo(function QueryResultsChart({
10541076
yAxisProps={yAxisProps}
10551077
stacked={stacked && sortedSeries.length > 1}
10561078
tooltipLabelFormatter={tooltipLabelFormatter}
1079+
tooltipValueFormatter={tooltipValueFormatter}
10571080
lineType="linear"
10581081
/>
10591082
</Chart.Root>
10601083
);
10611084
});
10621085

10631086
/**
1064-
* Creates a Y-axis value formatter based on the data range
1087+
* Creates a Y-axis value formatter based on the data range and optional format hint
10651088
*/
1066-
function createYAxisFormatter(data: Record<string, unknown>[], series: string[]) {
1089+
function createYAxisFormatter(
1090+
data: Record<string, unknown>[],
1091+
series: string[],
1092+
format?: ColumnFormatType
1093+
) {
10671094
// Find min and max values across all series
10681095
let minVal = Infinity;
10691096
let maxVal = -Infinity;
@@ -1080,6 +1107,46 @@ function createYAxisFormatter(data: Record<string, unknown>[], series: string[])
10801107

10811108
const range = maxVal - minVal;
10821109

1110+
// Format-aware formatters
1111+
if (format === "bytes" || format === "decimalBytes") {
1112+
const divisor = format === "bytes" ? 1024 : 1000;
1113+
const units =
1114+
format === "bytes"
1115+
? ["B", "KiB", "MiB", "GiB", "TiB"]
1116+
: ["B", "KB", "MB", "GB", "TB"];
1117+
return (value: number): string => {
1118+
if (value === 0) return "0 B";
1119+
// Use consistent unit for all ticks based on max value
1120+
const i = Math.min(
1121+
Math.floor(Math.log(Math.abs(maxVal || 1)) / Math.log(divisor)),
1122+
units.length - 1
1123+
);
1124+
const scaled = value / Math.pow(divisor, i);
1125+
return `${scaled.toFixed(scaled < 10 ? 1 : 0)} ${units[i]}`;
1126+
};
1127+
}
1128+
1129+
if (format === "percent") {
1130+
return (value: number): string => `${value.toFixed(range < 1 ? 2 : 1)}%`;
1131+
}
1132+
1133+
if (format === "duration") {
1134+
return (value: number): string => formatDurationMilliseconds(value, { style: "short" });
1135+
}
1136+
1137+
if (format === "durationSeconds") {
1138+
return (value: number): string =>
1139+
formatDurationMilliseconds(value * 1000, { style: "short" });
1140+
}
1141+
1142+
if (format === "costInDollars" || format === "cost") {
1143+
return (value: number): string => {
1144+
const dollars = format === "cost" ? value / 100 : value;
1145+
return formatCurrencyAccurate(dollars);
1146+
};
1147+
}
1148+
1149+
// Default formatter
10831150
return (value: number): string => {
10841151
// Use abbreviations for large numbers
10851152
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 { Paragraph } from "../primitives/Paragraph";
@@ -64,9 +65,10 @@ function getFormattedValue(value: unknown, column: OutputColumnMetadata): string
6465
if (value === null) return "NULL";
6566
if (value === undefined) return "";
6667

67-
// Handle custom render types
68-
if (column.customRenderType) {
69-
switch (column.customRenderType) {
68+
// Handle format hints (from prettyFormat() or auto-populated from customRenderType)
69+
const formatType = column.format ?? column.customRenderType;
70+
if (formatType) {
71+
switch (formatType) {
7072
case "duration":
7173
if (typeof value === "number") {
7274
return formatDurationMilliseconds(value, { style: "short" });
@@ -93,6 +95,26 @@ function getFormattedValue(value: unknown, column: OutputColumnMetadata): string
9395
return value;
9496
}
9597
break;
98+
case "bytes":
99+
if (typeof value === "number") {
100+
return formatBytes(value);
101+
}
102+
break;
103+
case "decimalBytes":
104+
if (typeof value === "number") {
105+
return formatDecimalBytes(value);
106+
}
107+
break;
108+
case "percent":
109+
if (typeof value === "number") {
110+
return `${value.toFixed(2)}%`;
111+
}
112+
break;
113+
case "quantity":
114+
if (typeof value === "number") {
115+
return formatQuantity(value);
116+
}
117+
break;
96118
}
97119
}
98120

@@ -220,6 +242,21 @@ function getDisplayLength(value: unknown, column: OutputColumnMetadata): number
220242
if (value === null) return 4; // "NULL"
221243
if (value === undefined) return 9; // "UNDEFINED"
222244

245+
// Handle format hint types - estimate their rendered width
246+
const fmt = column.format;
247+
if (fmt === "bytes" || fmt === "decimalBytes") {
248+
// e.g., "1.50 GiB" or "256.00 MB"
249+
return 12;
250+
}
251+
if (fmt === "percent") {
252+
// e.g., "45.23%"
253+
return 8;
254+
}
255+
if (fmt === "quantity") {
256+
// e.g., "1.50M"
257+
return 8;
258+
}
259+
223260
// Handle custom render types - estimate their rendered width
224261
if (column.customRenderType) {
225262
switch (column.customRenderType) {
@@ -392,6 +429,10 @@ function isRightAlignedColumn(column: OutputColumnMetadata): boolean {
392429
) {
393430
return true;
394431
}
432+
const fmt = column.format;
433+
if (fmt === "bytes" || fmt === "decimalBytes" || fmt === "percent" || fmt === "quantity") {
434+
return true;
435+
}
395436
return isNumericType(column.type);
396437
}
397438

@@ -474,6 +515,32 @@ function CellValue({
474515
return <pre className="text-text-dimmed">UNDEFINED</pre>;
475516
}
476517

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

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import type { OutputColumnMetadata } from "@internal/tsql";
1+
import type { ColumnFormatType, OutputColumnMetadata } from "@internal/tsql";
22
import { useMemo } from "react";
33
import type {
44
BigNumberAggregationType,
55
BigNumberConfiguration,
66
} from "~/components/metrics/QueryWidget";
7+
import { createValueFormatter } from "~/utils/columnFormat";
78
import { AnimatedNumber } from "../AnimatedNumber";
89
import { Spinner } from "../Spinner";
910
import { Paragraph } from "../Paragraph";
@@ -129,6 +130,15 @@ export function BigNumberCard({ rows, columns, config, isLoading = false }: BigN
129130
return aggregateValues(values, aggregation);
130131
}, [rows, column, aggregation, sortDirection]);
131132

133+
// Look up column format for format-aware display
134+
const columnValueFormatter = useMemo(() => {
135+
const columnMeta = columns.find((c) => c.name === column);
136+
const formatType = (columnMeta?.format ?? columnMeta?.customRenderType) as
137+
| ColumnFormatType
138+
| undefined;
139+
return createValueFormatter(formatType);
140+
}, [columns, column]);
141+
132142
if (isLoading) {
133143
return (
134144
<div className="grid h-full place-items-center [container-type:size]">
@@ -147,6 +157,21 @@ export function BigNumberCard({ rows, columns, config, isLoading = false }: BigN
147157
);
148158
}
149159

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

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
@@ -48,6 +48,8 @@ export type ChartBarRendererProps = {
4848
referenceLine?: ReferenceLineProps;
4949
/** Custom tooltip label formatter */
5050
tooltipLabelFormatter?: (label: string, payload: any[]) => string;
51+
/** Optional formatter for numeric tooltip values (e.g. bytes, duration) */
52+
tooltipValueFormatter?: (value: number) => string;
5153
/** Width injected by ResponsiveContainer */
5254
width?: number;
5355
/** Height injected by ResponsiveContainer */
@@ -72,6 +74,7 @@ export function ChartBarRenderer({
7274
yAxisProps: yAxisPropsProp,
7375
referenceLine,
7476
tooltipLabelFormatter,
77+
tooltipValueFormatter,
7578
width,
7679
height,
7780
}: ChartBarRendererProps) {
@@ -170,7 +173,7 @@ export function ChartBarRenderer({
170173
showLegend ? (
171174
() => null
172175
) : tooltipLabelFormatter ? (
173-
<ChartTooltipContent />
176+
<ChartTooltipContent valueFormatter={tooltipValueFormatter} />
174177
) : (
175178
<ZoomTooltip
176179
isSelecting={zoom?.isSelecting}

0 commit comments

Comments
 (0)