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" ;
@@ -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 ) {
0 commit comments