@@ -5,7 +5,15 @@ import {
55 ChartConfigWithOptDateRange ,
66 Filter ,
77} from '@hyperdx/common-utils/dist/types' ;
8- import { Box , Code , Container , Flex , Pagination , Text } from '@mantine/core' ;
8+ import {
9+ Box ,
10+ Code ,
11+ Container ,
12+ Divider ,
13+ Flex ,
14+ Pagination ,
15+ Text ,
16+ } from '@mantine/core' ;
917import { useElementSize } from '@mantine/hooks' ;
1018
1119import { isAggregateFunction } from '@/ChartUtils' ;
@@ -15,6 +23,8 @@ import { getFirstTimestampValueExpression } from '@/source';
1523import { SQLPreview } from './ChartSQLPreview' ;
1624import {
1725 getPropertyStatistics ,
26+ isDenylisted ,
27+ isHighCardinality ,
1828 mergeValueStatisticsMaps ,
1929} from './deltaChartUtils' ;
2030import {
@@ -194,52 +204,87 @@ export default function DBDeltaChart({
194204 limit : { limit : 1000 } ,
195205 } ) ;
196206
207+ // Column metadata for field classification (from ClickHouse response)
208+ const columnMeta = useMemo < { name : string ; type : string } [ ] > (
209+ ( ) => outlierData ?. meta ?? inlierData ?. meta ?? [ ] ,
210+ [ outlierData ?. meta , inlierData ?. meta ] ,
211+ ) ;
212+
197213 // TODO: Is loading state
198- const { sortedProperties, outlierValueOccurences, inlierValueOccurences } =
199- useMemo ( ( ) => {
200- const { percentageOccurences : outlierValueOccurences } =
201- getPropertyStatistics ( outlierData ?. data ?? [ ] ) ;
202-
203- const { percentageOccurences : inlierValueOccurences } =
204- getPropertyStatistics ( inlierData ?. data ?? [ ] ) ;
205-
206- // Get all the unique keys from the outliers
207- let uniqueKeys = new Set ( [ ...outlierValueOccurences . keys ( ) ] ) ;
208- // If there's no outliers, use inliers as the unique keys
209- if ( uniqueKeys . size === 0 ) {
210- uniqueKeys = new Set ( [ ...inlierValueOccurences . keys ( ) ] ) ;
214+ const {
215+ visibleProperties,
216+ hiddenProperties,
217+ outlierValueOccurences,
218+ inlierValueOccurences,
219+ } = useMemo ( ( ) => {
220+ const {
221+ percentageOccurences : outlierValueOccurences ,
222+ propertyOccurences : outlierPropertyOccurences ,
223+ valueOccurences : outlierRawValueOccurences ,
224+ } = getPropertyStatistics ( outlierData ?. data ?? [ ] ) ;
225+
226+ const {
227+ percentageOccurences : inlierValueOccurences ,
228+ propertyOccurences : inlierPropertyOccurences ,
229+ valueOccurences : inlierRawValueOccurences ,
230+ } = getPropertyStatistics ( inlierData ?. data ?? [ ] ) ;
231+
232+ // Get all the unique keys from the outliers
233+ let uniqueKeys = new Set ( [ ...outlierValueOccurences . keys ( ) ] ) ;
234+ // If there's no outliers, use inliers as the unique keys
235+ if ( uniqueKeys . size === 0 ) {
236+ uniqueKeys = new Set ( [ ...inlierValueOccurences . keys ( ) ] ) ;
237+ }
238+ // Now process the keys to find the ones with the highest delta between outlier and inlier percentages
239+ const sortedProperties = Array . from ( uniqueKeys )
240+ . map ( key => {
241+ const inlierCount =
242+ inlierValueOccurences . get ( key ) ?? new Map < string , number > ( ) ;
243+ const outlierCount =
244+ outlierValueOccurences . get ( key ) ?? new Map < string , number > ( ) ;
245+
246+ const mergedArray = mergeValueStatisticsMaps ( outlierCount , inlierCount ) ;
247+ let maxValueDelta = 0 ;
248+ mergedArray . forEach ( item => {
249+ const delta = Math . abs ( item . outlierCount - item . inlierCount ) ;
250+ if ( delta > maxValueDelta ) {
251+ maxValueDelta = delta ;
252+ }
253+ } ) ;
254+
255+ return [ key , maxValueDelta ] as const ;
256+ } )
257+ . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
258+ . map ( a => a [ 0 ] ) ;
259+
260+ // Split into visible (primary) and hidden (denylisted + high cardinality)
261+ const visibleProperties : string [ ] = [ ] ;
262+ const hiddenProperties : string [ ] = [ ] ;
263+ sortedProperties . forEach ( key => {
264+ if ( isDenylisted ( key , columnMeta ) ) {
265+ hiddenProperties . push ( key ) ;
266+ } else if (
267+ isHighCardinality (
268+ key ,
269+ outlierRawValueOccurences ,
270+ inlierRawValueOccurences ,
271+ outlierPropertyOccurences ,
272+ inlierPropertyOccurences ,
273+ )
274+ ) {
275+ hiddenProperties . push ( key ) ;
276+ } else {
277+ visibleProperties . push ( key ) ;
211278 }
212- // Now process the keys to find the ones with the highest delta between outlier and inlier percentages
213- const sortedProperties = Array . from ( uniqueKeys )
214- . map ( key => {
215- const inlierCount =
216- inlierValueOccurences . get ( key ) ?? new Map < string , number > ( ) ;
217- const outlierCount =
218- outlierValueOccurences . get ( key ) ?? new Map < string , number > ( ) ;
219-
220- const mergedArray = mergeValueStatisticsMaps (
221- outlierCount ,
222- inlierCount ,
223- ) ;
224- let maxValueDelta = 0 ;
225- mergedArray . forEach ( item => {
226- const delta = Math . abs ( item . outlierCount - item . inlierCount ) ;
227- if ( delta > maxValueDelta ) {
228- maxValueDelta = delta ;
229- }
230- } ) ;
231-
232- return [ key , maxValueDelta ] as const ;
233- } )
234- . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
235- . map ( a => a [ 0 ] ) ;
236-
237- return {
238- sortedProperties,
239- outlierValueOccurences,
240- inlierValueOccurences,
241- } ;
242- } , [ outlierData ?. data , inlierData ?. data ] ) ;
279+ } ) ;
280+
281+ return {
282+ visibleProperties,
283+ hiddenProperties,
284+ outlierValueOccurences,
285+ inlierValueOccurences,
286+ } ;
287+ } , [ outlierData ?. data , inlierData ?. data , columnMeta ] ) ;
243288
244289 const [ activePage , setPage ] = useState ( 1 ) ;
245290
@@ -260,11 +305,9 @@ export default function DBDeltaChart({
260305 ( CHART_HEIGHT + CHART_GAP ) ,
261306 ) ,
262307 ) ;
263- const PAGE_SIZE = columns * rows ;
264-
265308 useEffect ( ( ) => {
266309 setPage ( 1 ) ;
267- } , [ PAGE_SIZE , xMin , xMax , yMin , yMax ] ) ;
310+ } , [ columns , rows , xMin , xMax , yMin , yMax ] ) ;
268311
269312 if ( error ) {
270313 return (
@@ -301,7 +344,37 @@ export default function DBDeltaChart({
301344 ) ;
302345 }
303346
304- const totalPages = Math . ceil ( sortedProperties . length / PAGE_SIZE ) ;
347+ // Row-based pagination: visible and hidden sections are separate grids,
348+ // so an incomplete last row of visible fields does not get "filled" with hidden items.
349+ const visibleRows = Math . ceil ( visibleProperties . length / columns ) ;
350+ const hiddenRows = Math . ceil ( hiddenProperties . length / columns ) ;
351+ const totalRows = visibleRows + hiddenRows ;
352+ const totalPages = Math . ceil ( totalRows / rows ) ;
353+
354+ const pageRowStart = ( activePage - 1 ) * rows ;
355+ const pageRowEnd = activePage * rows ;
356+
357+ // Rows occupied by the visible section on this page
358+ const visRowStart = Math . min ( pageRowStart , visibleRows ) ;
359+ const visRowEnd = Math . min ( pageRowEnd , visibleRows ) ;
360+ const visibleOnPage = visibleProperties . slice (
361+ visRowStart * columns ,
362+ Math . min ( visRowEnd * columns , visibleProperties . length ) ,
363+ ) ;
364+
365+ // Rows occupied by the hidden section on this page
366+ const hidRowStart = Math . max ( 0 , pageRowStart - visibleRows ) ;
367+ const hidRowEnd = Math . min ( hiddenRows , Math . max ( 0 , pageRowEnd - visibleRows ) ) ;
368+ const hiddenOnPage = hiddenProperties . slice (
369+ hidRowStart * columns ,
370+ Math . min ( hidRowEnd * columns , hiddenProperties . length ) ,
371+ ) ;
372+
373+ // Show a divider when both sections appear on the same page
374+ const showDivider = visibleOnPage . length > 0 && hiddenOnPage . length > 0 ;
375+ // Show a header when ONLY hidden fields appear on this page (no divider above)
376+ const showHiddenHeader =
377+ hiddenOnPage . length > 0 && visibleOnPage . length === 0 ;
305378
306379 return (
307380 < Box
@@ -314,16 +387,16 @@ export default function DBDeltaChart({
314387 flexDirection : 'column' ,
315388 } }
316389 >
317- < div
318- style = { {
319- display : 'grid' ,
320- gridTemplateColumns : `repeat( ${ columns } , 1fr)` ,
321- gap : CHART_GAP ,
322- } }
323- >
324- { sortedProperties
325- . slice ( ( activePage - 1 ) * PAGE_SIZE , activePage * PAGE_SIZE )
326- . map ( property => (
390+ { /* Primary fields */ }
391+ { visibleOnPage . length > 0 && (
392+ < div
393+ style = { {
394+ display : 'grid' ,
395+ gridTemplateColumns : `repeat( ${ columns } , 1fr)` ,
396+ gap : CHART_GAP ,
397+ } }
398+ >
399+ { visibleOnPage . map ( property => (
327400 < PropertyComparisonChart
328401 name = { property }
329402 outlierValueOccurences = {
@@ -335,7 +408,44 @@ export default function DBDeltaChart({
335408 key = { property }
336409 />
337410 ) ) }
338- </ div >
411+ </ div >
412+ ) }
413+ { /* Divider between primary and lower-priority fields */ }
414+ { ( showDivider || showHiddenHeader ) && (
415+ < Divider
416+ mt = "lg"
417+ mb = "xs"
418+ label = {
419+ < Text size = "xs" c = "dimmed" >
420+ Lower-priority fields ({ hiddenProperties . length } )
421+ </ Text >
422+ }
423+ labelPosition = "left"
424+ />
425+ ) }
426+ { /* Lower-priority fields — separate grid so rows align independently */ }
427+ { hiddenOnPage . length > 0 && (
428+ < div
429+ style = { {
430+ display : 'grid' ,
431+ gridTemplateColumns : `repeat(${ columns } , 1fr)` ,
432+ gap : CHART_GAP ,
433+ } }
434+ >
435+ { hiddenOnPage . map ( key => (
436+ < PropertyComparisonChart
437+ name = { key }
438+ outlierValueOccurences = {
439+ outlierValueOccurences . get ( key ) ?? new Map ( )
440+ }
441+ inlierValueOccurences = {
442+ inlierValueOccurences . get ( key ) ?? new Map ( )
443+ }
444+ key = { key }
445+ />
446+ ) ) }
447+ </ div >
448+ ) }
339449 < Flex
340450 justify = "flex-end"
341451 style = { {
0 commit comments