Skip to content

Commit d4aa600

Browse files
refactor: extract deltaChartUtils and PropertyComparisonChart from DBDeltaChart (#1831)
## Summary Fixes #1830 Pure refactor — **zero behavior change**. Extracts inline utility functions and the `PropertyComparisonChart` component from `DBDeltaChart.tsx` (601 lines) into separate files. ## Changes | Action | File | Lines | |--------|------|-------| | **NEW** | `deltaChartUtils.ts` | 107 lines — `flattenData`, `getPropertyStatistics`, `mergeValueStatisticsMaps` | | **NEW** | `PropertyComparisonChart.tsx` | 130 lines — `PropertyComparisonChart`, `HDXBarChartTooltip`, layout constants | | **MODIFIED** | `DBDeltaChart.tsx` | 601 → 344 lines — inline code replaced with imports | **+257 / -257** — exact symmetry confirming this is a pure move. ## Why `DBDeltaChart.tsx` at 601 lines violates the 300-line guideline. More importantly, extracting into separate files enables the Event Deltas improvement series (#1823–#1829) to add features to clean, focused files instead of one monolith. ## Review guidance This is a **move-only refactor**. To verify: 1. `deltaChartUtils.ts` contains the exact functions from lines 52–156 of the old `DBDeltaChart.tsx`, now exported 2. `PropertyComparisonChart.tsx` contains the exact component from lines 158–259 and constants from lines 261–271, now exported 3. `DBDeltaChart.tsx` imports from the new files — no logic changes ## Test plan - [ ] Existing E2E tests pass - [ ] Event Deltas comparison view works identically - [ ] No visual changes 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 705dd1b commit d4aa600

3 files changed

Lines changed: 257 additions & 257 deletions

File tree

‎packages/app/src/components/DBDeltaChart.tsx‎

Lines changed: 12 additions & 257 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,4 @@
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';
112
import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse';
123
import {
134
ChartConfigWithDateRange,
@@ -20,255 +11,19 @@ import { useElementSize } from '@mantine/hooks';
2011
import { isAggregateFunction } from '@/ChartUtils';
2112
import { useQueriedChartConfig } from '@/hooks/useChartConfig';
2213
import { getFirstTimestampValueExpression } from '@/source';
23-
import {
24-
getChartColorError,
25-
getChartColorSuccess,
26-
truncateMiddle,
27-
} from '@/utils';
2814

2915
import { 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

27328
export default function DBDeltaChart({
27429
config,

0 commit comments

Comments
 (0)