@@ -40,6 +40,7 @@ import { useExplainQuery } from '@/hooks/useExplainQuery';
4040import {
4141 useAllFields ,
4242 useGetKeyValues ,
43+ useGetValuesDistribution ,
4344 useJsonColumns ,
4445 useTableMetadata ,
4546} from '@/hooks/useMetadata' ;
@@ -76,6 +77,8 @@ type FilterCheckboxProps = {
7677 onClickExclude ?: VoidFunction ;
7778 onClickPin : VoidFunction ;
7879 className ?: string ;
80+ percentage ?: number ;
81+ isPercentageLoading ?: boolean ;
7982} ;
8083
8184export const TextButton = ( {
@@ -105,6 +108,26 @@ export const TextButton = ({
105108 ) ;
106109} ;
107110
111+ type FilterPercentageProps = {
112+ percentage : number ;
113+ isLoading ?: boolean ;
114+ } ;
115+
116+ const FilterPercentage = ( { percentage, isLoading } : FilterPercentageProps ) => {
117+ const formattedPercentage =
118+ percentage < 1
119+ ? `<1%`
120+ : percentage >= 99.5
121+ ? `>99%`
122+ : `~${ Math . round ( percentage ) } %` ;
123+
124+ return (
125+ < Text size = "xs" c = "gray.3" className = { isLoading ? 'effect-pulse' : '' } >
126+ { formattedPercentage }
127+ </ Text >
128+ ) ;
129+ } ;
130+
108131const emptyFn = ( ) => { } ;
109132export const FilterCheckbox = ( {
110133 value,
@@ -115,6 +138,8 @@ export const FilterCheckbox = ({
115138 onClickExclude,
116139 onClickPin,
117140 className,
141+ percentage,
142+ isPercentageLoading,
118143} : FilterCheckboxProps ) => {
119144 return (
120145 < div
@@ -146,15 +171,30 @@ export const FilterCheckbox = ({
146171 fz = "xxs"
147172 color = "gray"
148173 >
149- < Text
150- size = "xs"
151- c = { value === 'excluded' ? 'red.4' : 'gray.3' }
152- truncate = "end"
174+ < Group
153175 w = "100%"
154- title = { label }
176+ gap = "xs"
177+ wrap = "nowrap"
178+ justify = "space-between"
179+ pe = { '11px' }
180+ miw = { 0 }
155181 >
156- { label }
157- </ Text >
182+ < Text
183+ size = "xs"
184+ c = { value === 'excluded' ? 'red.4' : 'gray.3' }
185+ truncate = "end"
186+ flex = { 1 }
187+ title = { label }
188+ >
189+ { label }
190+ </ Text >
191+ { percentage != null && (
192+ < FilterPercentage
193+ percentage = { percentage }
194+ isLoading = { isPercentageLoading }
195+ />
196+ ) }
197+ </ Group >
158198 </ Tooltip >
159199 </ Group >
160200 < div className = { classes . filterActions } >
@@ -208,6 +248,8 @@ export type FilterGroupProps = {
208248 hasLoadedMore : boolean ;
209249 isDefaultExpanded ?: boolean ;
210250 'data-testid' ?: string ;
251+ chartConfig : ChartConfigWithDateRange ;
252+ isLive ?: boolean ;
211253} ;
212254
213255const MAX_FILTER_GROUP_ITEMS = 10 ;
@@ -230,6 +272,8 @@ export const FilterGroup = ({
230272 hasLoadedMore,
231273 isDefaultExpanded,
232274 'data-testid' : dataTestId ,
275+ chartConfig,
276+ isLive,
233277} : FilterGroupProps ) => {
234278 const [ search , setSearch ] = useState ( '' ) ;
235279 // "Show More" button when there's lots of options
@@ -238,13 +282,60 @@ export const FilterGroup = ({
238282 const [ isExpanded , setExpanded ] = useState ( isDefaultExpanded ?? false ) ;
239283 // Track recently moved items for highlight animation
240284 const [ recentlyMoved , setRecentlyMoved ] = useState < Set < string > > ( new Set ( ) ) ;
285+ // Show what percentage of the data has each value
286+ const [ showDistributions , setShowDistributions ] = useState ( false ) ;
287+ // For live searches, don't refresh percentages when date range changes
288+ const [ dateRange , setDateRange ] = useState < [ Date , Date ] > (
289+ chartConfig . dateRange ,
290+ ) ;
291+
292+ const toggleShowDistributions = ( ) => {
293+ if ( ! showDistributions ) {
294+ setExpanded ( true ) ;
295+ setDateRange ( chartConfig . dateRange ) ;
296+ }
297+ setShowDistributions ( prev => ! prev ) ;
298+ } ;
299+
300+ useEffect ( ( ) => {
301+ if ( ! isLive ) {
302+ setDateRange ( chartConfig . dateRange ) ;
303+ }
304+ } , [ chartConfig . dateRange , isLive ] ) ;
241305
242306 useEffect ( ( ) => {
243307 if ( isDefaultExpanded ) {
244308 setExpanded ( true ) ;
245309 }
246310 } , [ isDefaultExpanded ] ) ;
247311
312+ const {
313+ data : distributionData ,
314+ isFetching : isFetchingDistribution ,
315+ error : distributionError ,
316+ } = useGetValuesDistribution (
317+ {
318+ chartConfig : { ...chartConfig , dateRange } ,
319+ key : name ,
320+ limit : 100 , // The 100 most common values are enough to find any values that are present in at least 1% of rows
321+ } ,
322+ {
323+ enabled : showDistributions ,
324+ } ,
325+ ) ;
326+
327+ useEffect ( ( ) => {
328+ if ( distributionError ) {
329+ notifications . show ( {
330+ color : 'red' ,
331+ title : 'Error loading filter distribution' ,
332+ message : distributionError ?. message ,
333+ autoClose : 5000 ,
334+ } ) ;
335+ setShowDistributions ( false ) ;
336+ }
337+ } , [ distributionError ] ) ;
338+
248339 const totalFiltersSize =
249340 selectedValues . included . size + selectedValues . excluded . size ;
250341
@@ -292,6 +383,13 @@ export const FilterGroup = ({
292383 if ( aExcluded && ! bExcluded ) return - 1 ;
293384 if ( ! aExcluded && bExcluded ) return 1 ;
294385
386+ // Then sort by estimated percentage of rows with this value, if available
387+ const aPercentage = distributionData ?. get ( a . value ) ?? 0 ;
388+ const bPercentage = distributionData ?. get ( b . value ) ?? 0 ;
389+ if ( aPercentage !== bPercentage ) {
390+ return bPercentage - aPercentage ;
391+ }
392+
295393 // Finally sort alphabetically/numerically
296394 return a . value . localeCompare ( b . value , undefined , { numeric : true } ) ;
297395 } ) ;
@@ -310,6 +408,7 @@ export const FilterGroup = ({
310408 augmentedOptions ,
311409 selectedValues ,
312410 totalFiltersSize ,
411+ distributionData ,
313412 ] ) ;
314413
315414 // Simple highlight animation when checkbox is checked
@@ -402,13 +501,30 @@ export const FilterGroup = ({
402501 </ Tooltip >
403502 </ Accordion . Control >
404503 < Group gap = "xxxs" wrap = "nowrap" >
504+ < ActionIcon
505+ size = "xs"
506+ variant = "subtle"
507+ color = "gray"
508+ onClick = { toggleShowDistributions }
509+ title = {
510+ showDistributions ? 'Hide distribution' : 'Show distribution'
511+ }
512+ data-testid = { `toggle-distribution-button-${ name } ` }
513+ aria-checked = { showDistributions }
514+ role = "checkbox"
515+ >
516+ < i
517+ className = { `bi ${ isFetchingDistribution ? 'spinner-border spinner-border-sm' : showDistributions ? 'bi-bar-chart-line-fill' : 'bi-bar-chart-line' } ` }
518+ />
519+ </ ActionIcon >
405520 { onFieldPinClick && (
406521 < ActionIcon
407522 size = "xs"
408523 variant = "subtle"
409524 color = "gray"
410525 onClick = { onFieldPinClick }
411526 title = { isFieldPinned ? 'Unpin field' : 'Pin field' }
527+ me = { '4px' }
412528 >
413529 < i
414530 className = { `bi bi-pin-angle${ isFieldPinned ? '-fill' : '' } ` }
@@ -452,6 +568,12 @@ export const FilterGroup = ({
452568 onClickOnly = { ( ) => onOnlyClick ( option . value ) }
453569 onClickExclude = { ( ) => onExcludeClick ( option . value ) }
454570 onClickPin = { ( ) => onPinClick ( option . value ) }
571+ isPercentageLoading = { isFetchingDistribution }
572+ percentage = {
573+ showDistributions && distributionData
574+ ? ( distributionData . get ( option . value ) ?? 0 )
575+ : undefined
576+ }
455577 />
456578 ) ) }
457579 { optionsLoading ? (
@@ -900,6 +1022,8 @@ const DBSearchPageFiltersComponent = ({
9001022 ( filterState [ facet . key ] . included . size > 0 ||
9011023 filterState [ facet . key ] . excluded . size > 0 ) )
9021024 }
1025+ chartConfig = { chartConfig }
1026+ isLive = { isLive }
9031027 />
9041028 ) ) }
9051029
0 commit comments