1- import type { OutputColumnMetadata } from "@internal/clickhouse" ;
1+ import type { ColumnFormatType , OutputColumnMetadata } from "@internal/clickhouse" ;
2+ import { formatDurationMilliseconds } from "@trigger.dev/core/v3" ;
23import { BarChart3 , LineChart } from "lucide-react" ;
34import { memo , useMemo } from "react" ;
5+ import { createValueFormatter } from "~/utils/columnFormat" ;
6+ import { formatCurrencyAccurate } from "~/utils/numberFormatter" ;
47import type { ChartConfig } from "~/components/primitives/charts/Chart" ;
58import { Chart } from "~/components/primitives/charts/ChartCompound" ;
69import { ChartBlankState } from "../primitives/charts/ChartBlankState" ;
@@ -855,8 +858,24 @@ export const QueryResultsChart = memo(function QueryResultsChart({
855858 } ;
856859 } , [ isDateBased , timeGranularity ] ) ;
857860
858- // Create dynamic Y-axis formatter based on data range
859- const yAxisFormatter = useMemo ( ( ) => createYAxisFormatter ( data , series ) , [ data , series ] ) ;
861+ // Resolve the Y-axis column format for formatting
862+ const yAxisFormat = useMemo ( ( ) => {
863+ if ( yAxisColumns . length === 0 ) return undefined ;
864+ const col = columns . find ( ( c ) => c . name === yAxisColumns [ 0 ] ) ;
865+ return ( col ?. format ?? col ?. customRenderType ) as ColumnFormatType | undefined ;
866+ } , [ yAxisColumns , columns ] ) ;
867+
868+ // Create dynamic Y-axis formatter based on data range and format
869+ const yAxisFormatter = useMemo (
870+ ( ) => createYAxisFormatter ( data , series , yAxisFormat ) ,
871+ [ data , series , yAxisFormat ]
872+ ) ;
873+
874+ // Create value formatter for tooltips and legend based on column format
875+ const tooltipValueFormatter = useMemo (
876+ ( ) => createValueFormatter ( yAxisFormat ) ,
877+ [ yAxisFormat ]
878+ ) ;
860879
861880 // Check if the group-by column has a runStatus customRenderType
862881 const groupByIsRunStatus = useMemo ( ( ) => {
@@ -1081,6 +1100,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
10811100 showLegend = { showLegend }
10821101 maxLegendItems = { fullLegend ? Infinity : 5 }
10831102 legendAggregation = { config . aggregation }
1103+ legendValueFormatter = { tooltipValueFormatter }
10841104 minHeight = "300px"
10851105 fillContainer
10861106 onViewAllLegendItems = { onViewAllLegendItems }
@@ -1093,6 +1113,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
10931113 yAxisProps = { yAxisProps }
10941114 stackId = { stacked ? "stack" : undefined }
10951115 tooltipLabelFormatter = { tooltipLabelFormatter }
1116+ tooltipValueFormatter = { tooltipValueFormatter }
10961117 />
10971118 </ Chart . Root >
10981119 ) ;
@@ -1110,6 +1131,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
11101131 showLegend = { showLegend }
11111132 maxLegendItems = { fullLegend ? Infinity : 5 }
11121133 legendAggregation = { config . aggregation }
1134+ legendValueFormatter = { tooltipValueFormatter }
11131135 minHeight = "300px"
11141136 fillContainer
11151137 onViewAllLegendItems = { onViewAllLegendItems }
@@ -1122,16 +1144,21 @@ export const QueryResultsChart = memo(function QueryResultsChart({
11221144 yAxisProps = { yAxisProps }
11231145 stacked = { stacked && visibleSeries . length > 1 }
11241146 tooltipLabelFormatter = { tooltipLabelFormatter }
1147+ tooltipValueFormatter = { tooltipValueFormatter }
11251148 lineType = "linear"
11261149 />
11271150 </ Chart . Root >
11281151 ) ;
11291152} ) ;
11301153
11311154/**
1132- * Creates a Y-axis value formatter based on the data range
1155+ * Creates a Y-axis value formatter based on the data range and optional format hint
11331156 */
1134- function createYAxisFormatter ( data : Record < string , unknown > [ ] , series : string [ ] ) {
1157+ function createYAxisFormatter (
1158+ data : Record < string , unknown > [ ] ,
1159+ series : string [ ] ,
1160+ format ?: ColumnFormatType
1161+ ) {
11351162 // Find min and max values across all series
11361163 let minVal = Infinity ;
11371164 let maxVal = - Infinity ;
@@ -1148,6 +1175,46 @@ function createYAxisFormatter(data: Record<string, unknown>[], series: string[])
11481175
11491176 const range = maxVal - minVal ;
11501177
1178+ // Format-aware formatters
1179+ if ( format === "bytes" || format === "decimalBytes" ) {
1180+ const divisor = format === "bytes" ? 1024 : 1000 ;
1181+ const units =
1182+ format === "bytes"
1183+ ? [ "B" , "KiB" , "MiB" , "GiB" , "TiB" ]
1184+ : [ "B" , "KB" , "MB" , "GB" , "TB" ] ;
1185+ return ( value : number ) : string => {
1186+ if ( value === 0 ) return "0 B" ;
1187+ // Use consistent unit for all ticks based on max value
1188+ const i = Math . min (
1189+ Math . max ( 0 , Math . floor ( Math . log ( Math . abs ( maxVal || 1 ) ) / Math . log ( divisor ) ) ) ,
1190+ units . length - 1
1191+ ) ;
1192+ const scaled = value / Math . pow ( divisor , i ) ;
1193+ return `${ scaled . toFixed ( scaled < 10 ? 1 : 0 ) } ${ units [ i ] } ` ;
1194+ } ;
1195+ }
1196+
1197+ if ( format === "percent" ) {
1198+ return ( value : number ) : string => `${ value . toFixed ( range < 1 ? 2 : 1 ) } %` ;
1199+ }
1200+
1201+ if ( format === "duration" ) {
1202+ return ( value : number ) : string => formatDurationMilliseconds ( value , { style : "short" } ) ;
1203+ }
1204+
1205+ if ( format === "durationSeconds" ) {
1206+ return ( value : number ) : string =>
1207+ formatDurationMilliseconds ( value * 1000 , { style : "short" } ) ;
1208+ }
1209+
1210+ if ( format === "costInDollars" || format === "cost" ) {
1211+ return ( value : number ) : string => {
1212+ const dollars = format === "cost" ? value / 100 : value ;
1213+ return formatCurrencyAccurate ( dollars ) ;
1214+ } ;
1215+ }
1216+
1217+ // Default formatter
11511218 return ( value : number ) : string => {
11521219 // Use abbreviations for large numbers
11531220 if ( Math . abs ( value ) >= 1_000_000 ) {
0 commit comments