@@ -16,43 +16,109 @@ import { StaleDataLoader } from "@/components/builder/widgets/common/StaleDataLo
1616import { WidgetStatusContainer } from "@/components/builder/widgets/common/WidgetStatusContainer" ;
1717
1818const DEFAULT_COLOR = "#0e58ff" ;
19+ const DEFAULT_HOVER_COLOR = "#3b82f6" ;
20+ const DEFAULT_SELECTED_COLOR = "#f5b704" ;
1921const OPACITY_MODIFIER = "33" ;
2022
2123export const CategoriesChartWidget = ( { config : rawConfig } : { config : CategoriesChartSchema } ) => {
2224 const { t, i18n } = useTranslation ( "common" ) ;
2325 const theme = useTheme ( ) ;
24- const { config, queryParams, layerId } = useChartWidget (
26+ const { config, queryParams, baseQueryParams , layerId } = useChartWidget (
2527 rawConfig ,
2628 categoriesChartConfigSchema ,
2729 aggregationStatsQueryParams
2830 ) ;
2931
32+ // Determine if we're in highlight mode
33+ const isHighlightMode = config ?. options ?. selection_response === "highlight" ;
34+
35+ // In highlight mode: always fetch full data for main display
36+ // In filter mode: use filtered data
37+ const mainQueryParams = isHighlightMode ? baseQueryParams : queryParams ;
38+
3039 const { aggregationStats, isLoading, isError } = useProjectLayerAggregationStats (
3140 layerId ,
41+ mainQueryParams as AggregationStatsQueryParams
42+ ) ;
43+
44+ // Fetch selected/filtered data (only in highlight mode)
45+ const { aggregationStats : selectedStats , isLoading : isSelectedLoading } = useProjectLayerAggregationStats (
46+ isHighlightMode ? layerId : undefined ,
3247 queryParams as AggregationStatsQueryParams
3348 ) ;
3449
3550 // Data handling
3651 const originalData = useMemo ( ( ) => aggregationStats ?. items || [ ] , [ aggregationStats ] ) ;
3752
53+ // Only show highlight visualization when there's actually filtered data
54+ const showHighlight = useMemo ( ( ) => {
55+ if ( ! isHighlightMode || ! selectedStats || ! aggregationStats ) return false ;
56+ // Calculate total counts to check if there's any filtering
57+ const totalMain = originalData . reduce ( ( sum , item ) => sum + item . operation_value , 0 ) ;
58+ const totalSelected = ( selectedStats . items || [ ] ) . reduce ( ( sum , item ) => sum + item . operation_value , 0 ) ;
59+ return totalSelected < totalMain ;
60+ } , [ isHighlightMode , selectedStats , aggregationStats , originalData ] ) ;
61+
62+ // Create a map of selected counts by category
63+ const selectedCountMap = useMemo ( ( ) => {
64+ if ( ! showHighlight || ! selectedStats ?. items ) return new Map < string , number > ( ) ;
65+ return new Map ( selectedStats . items . map ( ( item ) => [ item . grouped_value , item . operation_value ] ) ) ;
66+ } , [ showHighlight , selectedStats ] ) ;
67+
68+ // Apply custom order if defined
69+ const orderedData = useMemo ( ( ) => {
70+ if ( ! originalData . length ) return originalData ;
71+
72+ const customOrder = config ?. setup ?. custom_order ;
73+ if ( ! customOrder || customOrder . length === 0 ) {
74+ return originalData ;
75+ }
76+
77+ // Sort by custom order - items in customOrder come first in that order,
78+ // items not in customOrder are excluded
79+ const orderMap = new Map ( customOrder . map ( ( val , idx ) => [ val , idx ] ) ) ;
80+ return originalData
81+ . filter ( ( item ) => orderMap . has ( item . grouped_value ) )
82+ . sort ( ( a , b ) => {
83+ const aIdx = orderMap . get ( a . grouped_value ) ?? Infinity ;
84+ const bIdx = orderMap . get ( b . grouped_value ) ?? Infinity ;
85+ return aIdx - bIdx ;
86+ } ) ;
87+ } , [ originalData , config ?. setup ?. custom_order ] ) ;
88+
3889 const displayData = useMemo ( ( ) => {
39- if ( originalData . length > 0 ) return originalData ;
90+ if ( orderedData . length > 0 ) return orderedData ;
4091 return [ { grouped_value : t ( "no_data" ) , operation_value : 0 } ] ;
41- } , [ originalData , t ] ) ;
92+ } , [ orderedData , t ] ) ;
4293
4394 // Calculate max value for progress scaling
4495 const maxValue = useMemo ( ( ) => {
45- return originalData . length > 0 ? Math . max ( ...originalData . map ( ( item ) => item . operation_value ) ) : 1 ; // For "No data" state
46- } , [ originalData ] ) ;
96+ return orderedData . length > 0 ? Math . max ( ...orderedData . map ( ( item ) => item . operation_value ) ) : 1 ; // For "No data" state
97+ } , [ orderedData ] ) ;
4798
4899 const [ activeCategory , setActiveCategory ] = useState < string | undefined > ( ) ;
49100 const [ isHovering , setIsHovering ] = useState ( false ) ;
50101
51- const getColor = ( category : ( typeof displayData ) [ number ] ) => {
52- const baseColor = originalData . length > 0 ? config ?. options ?. color || DEFAULT_COLOR : "#e0e0e0" ;
53- const shouldDim = originalData . length === 0 || ( isHovering && activeCategory !== category . grouped_value ) ;
102+ // Colors
103+ const baseColor = config ?. options ?. color || DEFAULT_COLOR ;
104+ const hoverColor = config ?. options ?. highlight_color || DEFAULT_HOVER_COLOR ;
105+ const selectedColor = config ?. options ?. selected_color || DEFAULT_SELECTED_COLOR ;
106+
107+ const getColor = ( category : ( typeof displayData ) [ number ] , isSelected : boolean ) => {
108+ const isActive = activeCategory === category . grouped_value ;
109+ const hasData = orderedData . length > 0 ;
110+
111+ if ( ! hasData ) return "#e0e0e0" ;
112+
113+ // In highlight mode with selections shown
114+ if ( showHighlight && isSelected ) {
115+ if ( isActive ) return hoverColor ;
116+ return selectedColor ;
117+ }
54118
55- return shouldDim ? `${ baseColor } ${ OPACITY_MODIFIER } ` : baseColor ;
119+ if ( isActive ) return hoverColor ;
120+ if ( isHovering && ! isActive ) return `${ baseColor } ${ OPACITY_MODIFIER } ` ;
121+ return baseColor ;
56122 } ;
57123
58124 const isChartConfigured = useMemo ( ( ) => {
@@ -62,7 +128,7 @@ export const CategoriesChartWidget = ({ config: rawConfig }: { config: Categorie
62128 return (
63129 < >
64130 < WidgetStatusContainer
65- isLoading = { isLoading && ! aggregationStats && ! isError }
131+ isLoading = { ( isLoading || isSelectedLoading ) && ! aggregationStats && ! isError }
66132 isNotConfigured = { ! isChartConfigured }
67133 isError = { isError }
68134 height = { 150 }
@@ -84,7 +150,10 @@ export const CategoriesChartWidget = ({ config: rawConfig }: { config: Categorie
84150 p : 2 ,
85151 } } >
86152 { displayData . map ( ( category ) => {
87- const percentage = ( category . operation_value / maxValue ) * 100 ;
153+ const totalValue = category . operation_value ;
154+ const selectedValue = showHighlight ? selectedCountMap . get ( category . grouped_value ) || 0 : 0 ;
155+ const percentage = ( totalValue / maxValue ) * 100 ;
156+ const selectedPercentage = showHighlight ? ( selectedValue / maxValue ) * 100 : 0 ;
88157 const displayValue = formatNumber (
89158 category . operation_value ,
90159 config . options ?. format ,
@@ -110,28 +179,69 @@ export const CategoriesChartWidget = ({ config: rawConfig }: { config: Categorie
110179 { category . grouped_value }
111180 </ Typography >
112181 < Typography variant = "caption" fontWeight = { 500 } >
113- { displayValue }
182+ { showHighlight && selectedValue > 0 ? ` ${ selectedValue } / ${ displayValue } ` : displayValue }
114183 </ Typography >
115184 </ Box >
116- < LinearProgress
117- variant = "determinate"
118- value = { percentage }
119- sx = { {
120- height : 8 ,
121- borderRadius : 4 ,
122- backgroundColor : theme . palette . grey [ 200 ] ,
123- "& .MuiLinearProgress-bar" : {
185+ { showHighlight ? (
186+ // Stacked progress bar for highlight mode
187+ < Box sx = { { position : "relative" , height : 8 } } >
188+ { /* Base bar (full width for total) */ }
189+ < LinearProgress
190+ variant = "determinate"
191+ value = { percentage }
192+ sx = { {
193+ position : "absolute" ,
194+ width : "100%" ,
195+ height : 8 ,
196+ borderRadius : 4 ,
197+ backgroundColor : theme . palette . grey [ 200 ] ,
198+ "& .MuiLinearProgress-bar" : {
199+ borderRadius : 4 ,
200+ backgroundColor : getColor ( category , false ) ,
201+ } ,
202+ } }
203+ />
204+ { /* Selected bar (overlay showing selected portion) */ }
205+ { selectedValue > 0 && (
206+ < LinearProgress
207+ variant = "determinate"
208+ value = { selectedPercentage }
209+ sx = { {
210+ position : "absolute" ,
211+ width : "100%" ,
212+ height : 8 ,
213+ borderRadius : 4 ,
214+ backgroundColor : "transparent" ,
215+ "& .MuiLinearProgress-bar" : {
216+ borderRadius : 4 ,
217+ backgroundColor : getColor ( category , true ) ,
218+ } ,
219+ } }
220+ />
221+ ) }
222+ </ Box >
223+ ) : (
224+ // Normal single progress bar
225+ < LinearProgress
226+ variant = "determinate"
227+ value = { percentage }
228+ sx = { {
229+ height : 8 ,
124230 borderRadius : 4 ,
125- backgroundColor : getColor ( category ) ,
126- } ,
127- } }
128- />
231+ backgroundColor : theme . palette . grey [ 200 ] ,
232+ "& .MuiLinearProgress-bar" : {
233+ borderRadius : 4 ,
234+ backgroundColor : getColor ( category , false ) ,
235+ } ,
236+ } }
237+ />
238+ ) }
129239 </ Box >
130240 ) ;
131241 } ) }
132242 </ Box >
133243 ) }
134- < StaleDataLoader isLoading = { isLoading } hasData = { ! ! originalData . length } />
244+ < StaleDataLoader isLoading = { isLoading || isSelectedLoading } hasData = { ! ! orderedData . length } />
135245 </ >
136246 ) ;
137247} ;
0 commit comments