11import React , { useMemo } from "react" ;
22import type * as RechartsPrimitive from "recharts" ;
3+ import type { AggregationType } from "~/components/metrics/QueryWidget" ;
34import { ChartContainer , type ChartConfig , type ChartState } from "./Chart" ;
45import { ChartProvider , useChartContext , type LabelFormatter } from "./ChartContext" ;
56import { ChartLegendCompound } from "./ChartLegendCompound" ;
@@ -29,6 +30,8 @@ export type ChartRootProps = {
2930 maxLegendItems ?: number ;
3031 /** Label for the total row in the legend */
3132 legendTotalLabel ?: string ;
33+ /** Aggregation method used by the legend to compute totals (defaults to sum behavior) */
34+ legendAggregation ?: AggregationType ;
3235 /** Callback when "View all" legend button is clicked */
3336 onViewAllLegendItems ?: ( ) => void ;
3437 /** When true, constrains legend to max 50% height with scrolling */
@@ -73,6 +76,7 @@ export function ChartRoot({
7376 showLegend = false ,
7477 maxLegendItems = 5 ,
7578 legendTotalLabel,
79+ legendAggregation,
7680 onViewAllLegendItems,
7781 legendScrollable = false ,
7882 fillContainer = false ,
@@ -96,6 +100,7 @@ export function ChartRoot({
96100 showLegend = { showLegend }
97101 maxLegendItems = { maxLegendItems }
98102 legendTotalLabel = { legendTotalLabel }
103+ legendAggregation = { legendAggregation }
99104 onViewAllLegendItems = { onViewAllLegendItems }
100105 legendScrollable = { legendScrollable }
101106 fillContainer = { fillContainer }
@@ -112,6 +117,7 @@ type ChartRootInnerProps = {
112117 showLegend ?: boolean ;
113118 maxLegendItems ?: number ;
114119 legendTotalLabel ?: string ;
120+ legendAggregation ?: AggregationType ;
115121 onViewAllLegendItems ?: ( ) => void ;
116122 legendScrollable ?: boolean ;
117123 fillContainer ?: boolean ;
@@ -124,6 +130,7 @@ function ChartRootInner({
124130 showLegend = false ,
125131 maxLegendItems = 5 ,
126132 legendTotalLabel,
133+ legendAggregation,
127134 onViewAllLegendItems,
128135 legendScrollable = false ,
129136 fillContainer = false ,
@@ -165,6 +172,7 @@ function ChartRootInner({
165172 < ChartLegendCompound
166173 maxItems = { maxLegendItems }
167174 totalLabel = { legendTotalLabel }
175+ aggregation = { legendAggregation }
168176 onViewAllLegendItems = { onViewAllLegendItems }
169177 scrollable = { legendScrollable }
170178 />
@@ -194,18 +202,75 @@ export function useHasNoData(): boolean {
194202}
195203
196204/**
197- * Hook to calculate totals for each series across all data points.
205+ * Hook to calculate aggregated values for each series across all data points.
206+ * When no aggregation is provided, defaults to sum (original behavior).
198207 * Useful for legend displays.
199208 */
200- export function useSeriesTotal ( ) : Record < string , number > {
209+ export function useSeriesTotal ( aggregation ?: AggregationType ) : Record < string , number > {
201210 const { data, dataKeys } = useChartContext ( ) ;
202211
203212 return useMemo ( ( ) => {
204- return data . reduce ( ( acc , item ) => {
213+ // Sum (default) and count use additive accumulation
214+ if ( ! aggregation || aggregation === "sum" || aggregation === "count" ) {
215+ return data . reduce (
216+ ( acc , item ) => {
217+ for ( const seriesKey of dataKeys ) {
218+ acc [ seriesKey ] = ( acc [ seriesKey ] || 0 ) + Number ( item [ seriesKey ] || 0 ) ;
219+ }
220+ return acc ;
221+ } ,
222+ { } as Record < string , number >
223+ ) ;
224+ }
225+
226+ if ( aggregation === "avg" ) {
227+ const sums : Record < string , number > = { } ;
228+ const counts : Record < string , number > = { } ;
229+ for ( const item of data ) {
230+ for ( const seriesKey of dataKeys ) {
231+ const val = Number ( item [ seriesKey ] || 0 ) ;
232+ sums [ seriesKey ] = ( sums [ seriesKey ] || 0 ) + val ;
233+ counts [ seriesKey ] = ( counts [ seriesKey ] || 0 ) + 1 ;
234+ }
235+ }
236+ const result : Record < string , number > = { } ;
237+ for ( const key of dataKeys ) {
238+ result [ key ] = counts [ key ] ? sums [ key ] ! / counts [ key ] ! : 0 ;
239+ }
240+ return result ;
241+ }
242+
243+ if ( aggregation === "min" ) {
244+ const result : Record < string , number > = { } ;
245+ for ( const item of data ) {
246+ for ( const seriesKey of dataKeys ) {
247+ const val = Number ( item [ seriesKey ] || 0 ) ;
248+ if ( result [ seriesKey ] === undefined || val < result [ seriesKey ] ) {
249+ result [ seriesKey ] = val ;
250+ }
251+ }
252+ }
253+ // Default to 0 for series with no data
254+ for ( const key of dataKeys ) {
255+ if ( result [ key ] === undefined ) result [ key ] = 0 ;
256+ }
257+ return result ;
258+ }
259+
260+ // aggregation === "max"
261+ const result : Record < string , number > = { } ;
262+ for ( const item of data ) {
205263 for ( const seriesKey of dataKeys ) {
206- acc [ seriesKey ] = ( acc [ seriesKey ] || 0 ) + Number ( item [ seriesKey ] || 0 ) ;
264+ const val = Number ( item [ seriesKey ] || 0 ) ;
265+ if ( result [ seriesKey ] === undefined || val > result [ seriesKey ] ) {
266+ result [ seriesKey ] = val ;
267+ }
207268 }
208- return acc ;
209- } , { } as Record < string , number > ) ;
210- } , [ data , dataKeys ] ) ;
269+ }
270+ // Default to 0 for series with no data
271+ for ( const key of dataKeys ) {
272+ if ( result [ key ] === undefined ) result [ key ] = 0 ;
273+ }
274+ return result ;
275+ } , [ data , dataKeys , aggregation ] ) ;
211276}
0 commit comments