1- import { memo , useEffect , useMemo , useState } from 'react' ;
2- import { withErrorBoundary } from 'react-error-boundary' ;
3- import {
4- Bar ,
5- BarChart ,
6- ResponsiveContainer ,
7- Tooltip ,
8- XAxis ,
9- YAxis ,
10- } from 'recharts' ;
1+ import { useEffect , useMemo , useState } from 'react' ;
112import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse' ;
123import {
134 ChartConfigWithDateRange ,
@@ -20,255 +11,19 @@ import { useElementSize } from '@mantine/hooks';
2011import { isAggregateFunction } from '@/ChartUtils' ;
2112import { useQueriedChartConfig } from '@/hooks/useChartConfig' ;
2213import { getFirstTimestampValueExpression } from '@/source' ;
23- import {
24- getChartColorError ,
25- getChartColorSuccess ,
26- truncateMiddle ,
27- } from '@/utils' ;
2814
2915import { SQLPreview } from './ChartSQLPreview' ;
30-
31- import styles from '../../styles/HDXLineChart.module.scss' ;
32-
33- /*
34- * Response Data is like...
35- {
36- Timestamp: "",
37- Map: {
38- "property": value,
39- }
40- }
41-
42- - Flatten
43- - Count Property Occurences
44- - Pick most common properties
45- - Count values for most common properties
46-
47- - Merge both sets of properties? one property?
48- */
49-
50- // TODO: doesn't work for empty objects?
51- // https://stackoverflow.com/a/19101235
52- function flattenData ( data : Record < string , any > ) {
53- const result : Record < string , any > = { } ;
54- function recurse ( cur : Record < string , any > , prop : string ) {
55- if ( Object ( cur ) !== cur ) {
56- result [ prop ] = cur ;
57- } else if ( Array . isArray ( cur ) ) {
58- let l ;
59- for ( let i = 0 , l = cur . length ; i < l ; i ++ )
60- recurse ( cur [ i ] , prop + '[' + i + ']' ) ;
61- if ( l == 0 ) result [ prop ] = [ ] ;
62- } else {
63- let isEmpty = true ;
64- for ( const p in cur ) {
65- isEmpty = false ;
66- recurse ( cur [ p ] , prop ? prop + '.' + p : p ) ;
67- }
68- if ( isEmpty && prop ) result [ prop ] = { } ;
69- }
70- }
71- recurse ( data , '' ) ;
72- return result ;
73- }
74-
75- function getPropertyStatistics ( data : Record < string , any > [ ] ) {
76- const flattened = data . map ( flattenData ) ;
77- const propertyOccurences = new Map < string , number > ( ) ;
78-
79- const MIN_PROPERTY_OCCURENCES = 5 ;
80- const commonProperties = new Set < string > ( ) ;
81-
82- flattened . forEach ( item => {
83- Object . entries ( item ) . forEach ( ( [ key , value ] ) => {
84- const count = propertyOccurences . get ( key ) || 0 ;
85- propertyOccurences . set ( key , count + 1 ) ;
86-
87- if ( count + 1 >= MIN_PROPERTY_OCCURENCES ) {
88- commonProperties . add ( key ) ;
89- }
90- } ) ;
91- } ) ;
92-
93- // property -> (value -> count)
94- const valueOccurences = new Map < string , Map < string , number > > ( ) ;
95- flattened . forEach ( item => {
96- Object . entries ( item ) . forEach ( ( [ key , value ] ) => {
97- if ( commonProperties . has ( key ) ) {
98- let valuesMap = valueOccurences . get ( key ) ;
99- if ( ! valuesMap ) {
100- valuesMap = new Map < string , number > ( ) ;
101- valueOccurences . set ( key , valuesMap ) ;
102- }
103-
104- const valueCount = valuesMap . get ( value ) || 0 ;
105- valuesMap . set ( value , valueCount + 1 ) ;
106- }
107- } ) ;
108- } ) ;
109-
110- const percentageOccurences = new Map < string , Map < string , number > > ( ) ;
111- valueOccurences . forEach ( ( valuesMap , property ) => {
112- const percentageMap = new Map < string , number > ( ) ;
113- valuesMap . forEach ( ( valueCount , value ) => {
114- percentageMap . set (
115- value ,
116- ( valueCount / ( propertyOccurences . get ( property ) ?? 0 ) ) * 100 ,
117- ) ;
118- } ) ;
119- percentageOccurences . set ( property , percentageMap ) ;
120- } ) ;
121-
122- return {
123- // valueOccurences,
124- percentageOccurences,
125- // commonProperties,
126- // propertyOccurences,
127- } ;
128- }
129-
130- function mergeValueStatisticsMaps (
131- outlierValues : Map < string , number > , // value -> count
132- inlierValues : Map < string , number > ,
133- ) {
134- const mergedArray : {
135- name : string ;
136- outlierCount : number ;
137- inlierCount : number ;
138- } [ ] = [ ] ;
139- // Collect all value names for this property
140- // we sort them so timestamps are ordered
141- const allValues = Array . from (
142- new Set ( [ ...outlierValues . keys ( ) , ...inlierValues . keys ( ) ] ) ,
143- ) . sort ( ) ;
144-
145- allValues . forEach ( value => {
146- const count1 = outlierValues . get ( value ) || 0 ;
147- const count2 = inlierValues . get ( value ) || 0 ;
148- mergedArray . push ( {
149- name : value ,
150- outlierCount : count1 ,
151- inlierCount : count2 ,
152- } ) ;
153- } ) ;
154-
155- return mergedArray ;
156- }
157-
158- const HDXBarChartTooltip = withErrorBoundary (
159- memo ( ( props : any ) => {
160- const { active, payload, label, title } = props ;
161- if ( active && payload && payload . length ) {
162- return (
163- < div className = { styles . chartTooltip } >
164- < div className = { styles . chartTooltipContent } >
165- { title && (
166- < Text size = "xs" mb = "xs" >
167- { title }
168- </ Text >
169- ) }
170- < Text size = "xs" mb = "xs" >
171- { label . length === 0 ? < i > Empty String</ i > : label }
172- </ Text >
173- { payload
174- . sort ( ( a : any , b : any ) => b . value - a . value )
175- . map ( ( p : any ) => (
176- < div key = { p . dataKey } >
177- { p . name } : { p . value . toFixed ( 2 ) } %
178- </ div >
179- ) ) }
180- </ div >
181- </ div >
182- ) ;
183- }
184- return null ;
185- } ) ,
186- {
187- onError : console . error ,
188- fallback : (
189- < div className = "text-danger px-2 py-1 m-2 fs-8 font-monospace bg-danger-transparent" >
190- An error occurred while rendering the tooltip.
191- </ div >
192- ) ,
193- } ,
194- ) ;
195-
196- function PropertyComparisonChart ( {
197- name,
198- outlierValueOccurences,
199- inlierValueOccurences,
200- } : {
201- name : string ;
202- outlierValueOccurences : Map < string , number > ;
203- inlierValueOccurences : Map < string , number > ;
204- } ) {
205- const mergedValueStatistics = mergeValueStatisticsMaps (
206- outlierValueOccurences ,
207- inlierValueOccurences ,
208- ) ;
209-
210- return (
211- < div style = { { width : '100%' , height : 120 } } >
212- < Text size = "xs" ta = "center" title = { name } >
213- { truncateMiddle ( name , 32 ) }
214- </ Text >
215- < ResponsiveContainer width = "100%" height = "100%" >
216- < BarChart
217- barGap = { 2 }
218- width = { 500 }
219- height = { 300 }
220- data = { mergedValueStatistics }
221- margin = { {
222- top : 0 ,
223- right : 0 ,
224- left : 0 ,
225- bottom : 0 ,
226- } }
227- >
228- { /* <CartesianGrid strokeDasharray="3 3" /> */ }
229- < XAxis
230- dataKey = "name"
231- tick = { { fontSize : 10 , fontFamily : 'IBM Plex Mono, monospace' } }
232- />
233- < YAxis
234- tick = { { fontSize : 11 , fontFamily : 'IBM Plex Mono, monospace' } }
235- />
236- < Tooltip
237- wrapperStyle = { {
238- zIndex : 1000 ,
239- } }
240- content = { < HDXBarChartTooltip title = { name } /> }
241- allowEscapeViewBox = { { y : true } }
242- />
243- < Bar
244- dataKey = "outlierCount"
245- name = "Outliers"
246- fill = { getChartColorError ( ) }
247- isAnimationActive = { false }
248- />
249- < Bar
250- dataKey = "inlierCount"
251- name = "Inliers"
252- fill = { getChartColorSuccess ( ) }
253- isAnimationActive = { false }
254- />
255- </ BarChart >
256- </ ResponsiveContainer >
257- </ div >
258- ) ;
259- }
260-
261- // Layout constants for dynamic grid calculation.
262- // CHART_WIDTH is the minimum chart width used to determine how many columns fit; actual rendered
263- // width expands to fill the container (charts use width: '100%' inside a CSS grid).
264- // CHART_HEIGHT must match PropertyComparisonChart's outer div height.
265- // CHART_GAP is used both in the column/row formula and as the CSS grid gap.
266- const CHART_WIDTH = 340 ; // minimum column width threshold (px)
267- const CHART_HEIGHT = 120 ; // must match PropertyComparisonChart outer div height (px)
268- const CHART_GAP = 16 ; // px; used in grid gap and layout math
269- // Space reserved for the pagination row: Pagination control (~32px) + top padding (16px).
270- // Always reserved (even when pagination is hidden via visibility:hidden) so rows count is stable.
271- const PAGINATION_HEIGHT = 48 ;
16+ import {
17+ getPropertyStatistics ,
18+ mergeValueStatisticsMaps ,
19+ } from './deltaChartUtils' ;
20+ import {
21+ CHART_GAP ,
22+ CHART_HEIGHT ,
23+ CHART_WIDTH ,
24+ PAGINATION_HEIGHT ,
25+ PropertyComparisonChart ,
26+ } from './PropertyComparisonChart' ;
27227
27328export default function DBDeltaChart ( {
27429 config,
0 commit comments