@parca/profile 0.19.43 → 0.19.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/GraphTooltipArrow/Content.d.ts.map +1 -1
  3. package/dist/GraphTooltipArrow/Content.js +1 -1
  4. package/dist/MetricsGraph/MetricsContextMenu/index.d.ts +20 -11
  5. package/dist/MetricsGraph/MetricsContextMenu/index.d.ts.map +1 -1
  6. package/dist/MetricsGraph/MetricsContextMenu/index.js +16 -20
  7. package/dist/MetricsGraph/MetricsTooltip/index.d.ts +2 -8
  8. package/dist/MetricsGraph/MetricsTooltip/index.d.ts.map +1 -1
  9. package/dist/MetricsGraph/MetricsTooltip/index.js +46 -55
  10. package/dist/MetricsGraph/UtilizationMetrics/Throughput.d.ts +2 -5
  11. package/dist/MetricsGraph/UtilizationMetrics/Throughput.d.ts.map +1 -1
  12. package/dist/MetricsGraph/UtilizationMetrics/Throughput.js +126 -205
  13. package/dist/MetricsGraph/UtilizationMetrics/index.d.ts +9 -17
  14. package/dist/MetricsGraph/UtilizationMetrics/index.d.ts.map +1 -1
  15. package/dist/MetricsGraph/UtilizationMetrics/index.js +149 -208
  16. package/dist/MetricsGraph/index.d.ts +19 -26
  17. package/dist/MetricsGraph/index.d.ts.map +1 -1
  18. package/dist/MetricsGraph/index.js +50 -115
  19. package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
  20. package/dist/ProfileFlameGraph/index.js +3 -1
  21. package/dist/ProfileMetricsGraph/index.d.ts +1 -1
  22. package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
  23. package/dist/ProfileMetricsGraph/index.js +232 -23
  24. package/dist/ProfileSelector/MetricsGraphSection.d.ts +1 -4
  25. package/dist/ProfileSelector/MetricsGraphSection.d.ts.map +1 -1
  26. package/dist/ProfileSelector/MetricsGraphSection.js +8 -4
  27. package/dist/ProfileSelector/QueryControls.d.ts.map +1 -1
  28. package/dist/ProfileSelector/QueryControls.js +3 -2
  29. package/dist/ProfileSelector/index.d.ts +3 -6
  30. package/dist/ProfileSelector/index.d.ts.map +1 -1
  31. package/dist/ProfileSelector/index.js +2 -2
  32. package/dist/ProfileSource.d.ts +9 -6
  33. package/dist/ProfileSource.d.ts.map +1 -1
  34. package/dist/ProfileSource.js +23 -8
  35. package/dist/styles.css +1 -1
  36. package/dist/useQuery.js +1 -1
  37. package/package.json +6 -6
  38. package/src/GraphTooltipArrow/Content.tsx +2 -4
  39. package/src/MetricsGraph/MetricsContextMenu/index.tsx +78 -66
  40. package/src/MetricsGraph/MetricsTooltip/index.tsx +53 -210
  41. package/src/MetricsGraph/UtilizationMetrics/Throughput.tsx +242 -434
  42. package/src/MetricsGraph/UtilizationMetrics/index.tsx +312 -448
  43. package/src/MetricsGraph/index.tsx +99 -185
  44. package/src/ProfileFlameGraph/index.tsx +3 -1
  45. package/src/ProfileMetricsGraph/index.tsx +430 -37
  46. package/src/ProfileSelector/MetricsGraphSection.tsx +12 -8
  47. package/src/ProfileSelector/QueryControls.tsx +4 -1
  48. package/src/ProfileSelector/index.tsx +5 -5
  49. package/src/ProfileSource.tsx +34 -17
  50. package/src/useQuery.tsx +1 -1
@@ -11,23 +11,22 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import {Fragment, useCallback, useId, useMemo, useRef, useState} from 'react';
14
+ import {useMemo} from 'react';
15
15
 
16
- import * as d3 from 'd3';
17
- import {pointer} from 'd3-selection';
16
+ import {Icon} from '@iconify/react';
18
17
  import {AnimatePresence, motion} from 'framer-motion';
19
- import throttle from 'lodash.throttle';
20
- import {useContextMenu} from 'react-contexify';
21
18
 
22
- import {DateTimeRange, MetricsGraphSkeleton, useParcaContext, useURLState} from '@parca/components';
23
- import {Matcher} from '@parca/parser';
24
- import {formatDate, formatForTimespan, getPrecision, valueFormatter} from '@parca/utilities';
19
+ import {
20
+ DateTimeRange,
21
+ MetricsGraphSkeleton,
22
+ TextWithTooltip,
23
+ useParcaContext,
24
+ } from '@parca/components';
25
+ import {formatDate, timePattern, valueFormatter} from '@parca/utilities';
25
26
 
26
27
  import {type UtilizationMetrics as MetricSeries} from '../../ProfileSelector';
27
- import MetricsContextMenu from '../MetricsContextMenu';
28
- import MetricsTooltip from '../MetricsTooltip';
28
+ import MetricsGraph, {type ContextMenuItemOrSubmenu, type Series} from '../index';
29
29
  import {useMetricsGraphDimensions} from '../useMetricsGraphDimensions';
30
- import {getSeriesColor} from '../utils/colorMapping';
31
30
 
32
31
  interface NetworkLabel {
33
32
  name: string;
@@ -53,19 +52,116 @@ interface CommonProps {
53
52
  from: number;
54
53
  to: number;
55
54
  selectedSeries?: Array<{key: string; value: string}>;
56
- onSelectedSeriesChange?: (series: Array<{key: string; value: string}>) => void;
55
+ onSeriesClick?: (seriesIndex: number) => void;
57
56
  }
58
57
 
59
58
  type RawAreaChartProps = CommonProps & {
59
+ transformedData: Series[];
60
60
  width: number;
61
61
  height: number;
62
62
  margin: number;
63
+ contextMenuItems?: ContextMenuItemOrSubmenu[];
63
64
  };
64
65
 
65
66
  type Props = CommonProps & {
66
67
  utilizationMetricsLoading?: boolean;
67
68
  };
68
69
 
70
+ const transformUtilizationLabels = (label: string): string => {
71
+ return label.replace('attributes.', '').replace('attributes_resource.', '');
72
+ };
73
+
74
+ const createThroughputContextMenuItems = (
75
+ addLabelMatcher: (
76
+ labels: {key: string; value: string} | Array<{key: string; value: string}>
77
+ ) => void,
78
+ transmitData: MetricSeries[],
79
+ receiveData: MetricSeries[]
80
+ ): ContextMenuItemOrSubmenu[] => {
81
+ const allData = [...transmitData, ...receiveData];
82
+
83
+ return [
84
+ {
85
+ id: 'focus-on-single-series',
86
+ label: 'Focus only on this series',
87
+ icon: 'ph:star',
88
+ onClick: (closestPoint, _series) => {
89
+ if (
90
+ closestPoint != null &&
91
+ allData.length > 0 &&
92
+ allData[closestPoint.seriesIndex] != null
93
+ ) {
94
+ const originalSeriesData = allData[closestPoint.seriesIndex];
95
+ if (originalSeriesData.labelset?.labels != null) {
96
+ const labels = originalSeriesData.labelset.labels.filter(
97
+ label => label.name !== '__name__'
98
+ );
99
+ const labelsToAdd = labels.map(label => ({
100
+ key: label.name,
101
+ value: label.value,
102
+ }));
103
+ addLabelMatcher(labelsToAdd);
104
+ }
105
+ }
106
+ },
107
+ },
108
+ {
109
+ id: 'add-to-query',
110
+ label: 'Add to query',
111
+ icon: 'material-symbols:add',
112
+ createDynamicItems: (closestPoint, _series) => {
113
+ if (
114
+ closestPoint == null ||
115
+ allData.length === 0 ||
116
+ allData[closestPoint.seriesIndex] == null
117
+ ) {
118
+ return [
119
+ {
120
+ id: 'no-labels-available',
121
+ label: 'No labels available',
122
+ icon: 'ph:warning',
123
+ disabled: () => true,
124
+ onClick: () => {}, // No-op for disabled item
125
+ },
126
+ ];
127
+ }
128
+
129
+ const originalSeriesData = allData[closestPoint.seriesIndex];
130
+ if (originalSeriesData.labelset?.labels == null) {
131
+ return [
132
+ {
133
+ id: 'no-labels-available',
134
+ label: 'No labels available',
135
+ icon: 'ph:warning',
136
+ disabled: () => true,
137
+ onClick: () => {}, // No-op for disabled item
138
+ },
139
+ ];
140
+ }
141
+
142
+ const labels = originalSeriesData.labelset.labels.filter(
143
+ label => label.name !== '__name__'
144
+ );
145
+
146
+ return labels.map(label => ({
147
+ id: `add-label-${label.name}`,
148
+ label: (
149
+ <div className="mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-300">
150
+ {`${transformUtilizationLabels(label.name)}="${label.value}"`}
151
+ </div>
152
+ ),
153
+ onClick: () => {
154
+ addLabelMatcher({
155
+ key: label.name,
156
+ value: label.value,
157
+ });
158
+ },
159
+ }));
160
+ },
161
+ },
162
+ ];
163
+ };
164
+
69
165
  interface MetricsSample {
70
166
  timestamp: number;
71
167
  value: number;
@@ -103,10 +199,39 @@ function transformToSeries(data: MetricSeries[], isReceive = false): NetworkSeri
103
199
  }));
104
200
  }
105
201
 
202
+ function transformNetworkSeriesToSeries(
203
+ transmitData: MetricSeries[],
204
+ receiveData: MetricSeries[]
205
+ ): Series[] {
206
+ const transmitSeries = transformToSeries(transmitData);
207
+ const receiveSeries = transformToSeries(receiveData, true);
208
+ const allSeries = [...transmitSeries, ...receiveSeries];
209
+
210
+ return allSeries.map(networkSeries => {
211
+ const labels = networkSeries.metric ?? [];
212
+ const sortedLabels = labels
213
+ .filter(label => label.name !== '__name__')
214
+ .sort((a, b) => a.name.localeCompare(b.name));
215
+ const labelString = sortedLabels.map(label => `${label.name}=${label.value}`).join(',');
216
+ const id =
217
+ (networkSeries.isReceive === true ? 'receive-' : 'transmit-') +
218
+ (labelString !== '' ? labelString : 'default');
219
+
220
+ return {
221
+ id,
222
+ values: networkSeries.values.map(([timestamp, value]): [number, number] => [
223
+ timestamp,
224
+ value,
225
+ ]),
226
+ };
227
+ });
228
+ }
229
+
106
230
  const RawAreaChart = ({
107
231
  transmitData,
108
232
  receiveData,
109
- addLabelMatcher,
233
+ transformedData,
234
+ addLabelMatcher: _addLabelMatcher,
110
235
  setTimeRange,
111
236
  width,
112
237
  height,
@@ -114,431 +239,103 @@ const RawAreaChart = ({
114
239
  humanReadableName,
115
240
  from,
116
241
  to,
117
- selectedSeries,
118
- onSelectedSeriesChange,
242
+ selectedSeries: _selectedSeries,
243
+ onSeriesClick,
244
+ contextMenuItems,
119
245
  }: RawAreaChartProps): JSX.Element => {
120
246
  const {timezone} = useParcaContext();
121
- const graph = useRef(null);
122
- const [dragging, setDragging] = useState(false);
123
- const [hovering, setHovering] = useState(false);
124
- const [relPos, setRelPos] = useState(-1);
125
- const [pos, setPos] = useState([0, 0]);
126
- const [isContextMenuOpen, setIsContextMenuOpen] = useState<boolean>(false);
127
- const idForContextMenu = useId();
128
- const [_, setSelectedTimeframe] = useURLState('gpu_selected_timeframe');
129
-
130
- const parsedSelectedSeries: Matcher[] = useMemo(() => {
131
- if (selectedSeries == null || selectedSeries.length === 0) {
132
- return [];
133
- }
134
-
135
- return selectedSeries.map(s => ({
136
- key: s.key,
137
- value: s.value,
138
- matcherType: '=' as const,
139
- }));
140
- }, [selectedSeries]);
141
-
142
- const lineStroke = '1px';
143
- const lineStrokeHover = '2px';
144
- const lineStrokeSelected = '3px';
145
-
146
- const graphWidth = width - margin * 1.5 - margin / 2;
147
-
148
- const paddedFrom = from;
149
- const paddedTo = to;
150
-
151
- const series = useMemo(() => {
152
- const transmitSeries = transformToSeries(transmitData);
153
- const receiveSeries = transformToSeries(receiveData, true);
154
- return [...transmitSeries, ...receiveSeries];
155
- }, [transmitData, receiveData]);
156
-
157
- const extentsY = series.map(function (s) {
158
- return d3.extent(s.values, function (d) {
159
- return d[1];
160
- });
161
- });
162
-
163
- const minY = d3.min(extentsY, function (d) {
164
- return d[0];
165
- });
166
-
167
- const maxY = d3.max(extentsY, function (d) {
168
- return d[1];
169
- });
170
-
171
- // Setup scales with padded time range
172
- const xScale = d3.scaleUtc().domain([paddedFrom, paddedTo]).range([0, graphWidth]);
173
-
174
- const yScale = d3
175
- .scaleLinear()
176
- // Ensure domain is symmetric around 0 for balanced visualization
177
- .domain([minY ?? 0, maxY ?? 0])
178
- .range([height - margin, 0])
179
- .nice();
180
-
181
- const throttledSetPos = throttle(setPos, 20);
182
-
183
- const onMouseMove = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
184
- if (isContextMenuOpen) {
185
- return;
186
- }
187
-
188
- // X/Y coordinate array relative to svg
189
- const rel = pointer(e);
190
-
191
- const xCoordinate = rel[0];
192
- const xCoordinateWithoutMargin = xCoordinate - margin;
193
- const yCoordinate = rel[1];
194
- const yCoordinateWithoutMargin = yCoordinate - margin;
195
-
196
- throttledSetPos([xCoordinateWithoutMargin, yCoordinateWithoutMargin]);
197
- };
198
-
199
- const trackVisibility = (isVisible: boolean): void => {
200
- setIsContextMenuOpen(isVisible);
201
- };
202
-
203
- const MENU_ID = `areachart-context-menu-${idForContextMenu}`;
204
247
 
205
- const {show} = useContextMenu({
206
- id: MENU_ID,
207
- });
208
-
209
- const displayMenu = useCallback(
210
- (e: React.MouseEvent): void => {
211
- show({
212
- event: e,
213
- });
214
- },
215
- [show]
248
+ // Compute original series data for rich tooltip
249
+ const allOriginalData = useMemo(
250
+ () => [...transmitData, ...receiveData],
251
+ [transmitData, receiveData]
216
252
  );
217
253
 
218
- // Create line generator for both transmit and receive
219
- const lineGenerator = d3
220
- .line<number[]>()
221
- .x(d => xScale(d[0]))
222
- .y(d => yScale(d[1]));
223
-
224
- const highlighted = useMemo(() => {
225
- if (series.length === 0) {
226
- return null;
227
- }
228
-
229
- // Return the closest point as the highlighted point
230
- const closestPointPerSeries = series.map(function (s) {
231
- const distances = s.values.map(d => {
232
- const x = xScale(d[0]) + margin / 2;
233
- const y = yScale(d[1]) - margin / 3;
234
-
235
- return Math.sqrt(Math.pow(pos[0] - x, 2) + Math.pow(pos[1] - y, 2));
236
- });
237
-
238
- const pointIndex = d3.minIndex(distances);
239
- const minDistance = distances[pointIndex];
240
-
241
- return {
242
- pointIndex,
243
- distance: minDistance,
244
- };
245
- });
246
-
247
- const closestSeriesIndex = d3.minIndex(closestPointPerSeries, s => s.distance);
248
- const pointIndex = closestPointPerSeries[closestSeriesIndex].pointIndex;
249
- const point = series[closestSeriesIndex].values[pointIndex];
250
- return {
251
- seriesIndex: closestSeriesIndex,
252
- labels: series[closestSeriesIndex].metric,
253
- timestamp: point[0],
254
- valuePerSecond: point[1],
255
- value: point[2],
256
- duration: point[3],
257
- x: xScale(point[0]),
258
- y: yScale(point[1]),
259
- };
260
- }, [pos, series, xScale, yScale, margin]);
261
-
262
- const onMouseDown = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
263
- // only left mouse button
264
- if (e.button !== 0) {
265
- return;
266
- }
267
-
268
- // X/Y coordinate array relative to svg
269
- const rel = pointer(e);
270
-
271
- const xCoordinate = rel[0];
272
- const xCoordinateWithoutMargin = xCoordinate - margin;
273
- if (xCoordinateWithoutMargin >= 0) {
274
- setRelPos(xCoordinateWithoutMargin);
275
- setDragging(true);
276
- }
277
-
278
- e.stopPropagation();
279
- e.preventDefault();
280
- };
281
-
282
- const onMouseUp = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
283
- setDragging(false);
284
-
285
- if (relPos === -1) {
286
- // MouseDown happened outside of this element.
287
- return;
288
- }
289
-
290
- // This is a normal click. We tolerate tiny movements to still be a
291
- // click as they can occur when clicking based on user feedback.
292
- if (Math.abs(relPos - pos[0]) <= 1) {
293
- setRelPos(-1);
294
- return;
295
- }
296
-
297
- let startPos = relPos;
298
- let endPos = pos[0];
299
-
300
- if (startPos > endPos) {
301
- startPos = pos[0];
302
- endPos = relPos;
303
- }
304
-
305
- const startCorrection = 10;
306
- const endCorrection = 30;
307
-
308
- const firstTime = xScale.invert(startPos - startCorrection).valueOf();
309
- const secondTime = xScale.invert(endPos - endCorrection).valueOf();
310
-
311
- setTimeRange(DateTimeRange.fromAbsoluteDates(firstTime, secondTime));
312
-
313
- setRelPos(-1);
314
-
315
- e.stopPropagation();
316
- e.preventDefault();
317
- };
318
-
319
254
  return (
320
- <>
321
- <MetricsContextMenu
322
- onAddLabelMatcher={addLabelMatcher}
323
- menuId={MENU_ID}
324
- highlighted={highlighted}
325
- trackVisibility={trackVisibility}
326
- utilizationMetrics={true}
327
- />
328
-
329
- {highlighted != null && hovering && !dragging && pos[0] !== 0 && pos[1] !== 0 && (
330
- <div
331
- onMouseMove={onMouseMove}
332
- onMouseEnter={() => setHovering(true)}
333
- onMouseLeave={() => setHovering(false)}
334
- >
335
- {!isContextMenuOpen && (
336
- <MetricsTooltip
337
- x={pos[0] + margin}
338
- y={pos[1] + margin}
339
- highlighted={{
340
- ...highlighted,
341
- valuePerSecond: Math.abs(highlighted.valuePerSecond),
342
- }}
343
- contextElement={graph.current}
344
- sampleType={'throughput'}
345
- sampleUnit={'bytes_per_second'}
346
- delta={false}
347
- utilizationMetrics={true}
348
- valuePrefix={
349
- highlighted.seriesIndex >= transmitData.length ? 'Receive ' : 'Transmit '
350
- }
351
- />
352
- )}
353
- </div>
354
- )}
355
- <div
356
- ref={graph}
357
- onMouseEnter={() => setHovering(true)}
358
- onMouseLeave={() => setHovering(false)}
359
- onContextMenu={displayMenu}
360
- >
361
- <svg
362
- width={`${width}px`}
363
- height={`${height + margin}px`}
364
- onMouseDown={onMouseDown}
365
- onMouseUp={onMouseUp}
366
- onMouseMove={onMouseMove}
367
- >
368
- <g transform={`translate(${margin}, 0)`}>
369
- {dragging && (
370
- <g className="zoom-time-rect">
371
- <rect
372
- className="bar"
373
- x={pos[0] - relPos < 0 ? pos[0] : relPos}
374
- y={0}
375
- height={height}
376
- width={Math.abs(pos[0] - relPos)}
377
- fill={'rgba(0, 0, 0, 0.125)'}
378
- />
379
- </g>
380
- )}
381
- </g>
382
- <g transform={`translate(${margin * 1.5}, ${margin / 1.5})`}>
383
- <g className="y axis" textAnchor="end" fontSize="10" fill="none">
384
- {yScale.ticks(6).map((d, i, allTicks) => {
385
- let decimals = 2;
386
- const intervalBetweenTicks = allTicks[1] - allTicks[0];
387
-
388
- if (intervalBetweenTicks < 1) {
389
- const precision = getPrecision(intervalBetweenTicks);
390
- decimals = precision;
391
- }
392
-
393
- return (
394
- <Fragment key={`${i.toString()}-${d.toString()}`}>
395
- <g key={`tick-${i}`} className="tick" transform={`translate(0, ${yScale(d)})`}>
396
- <line className="stroke-gray-300 dark:stroke-gray-500" x2={-6} />
397
- <text fill="currentColor" x={-9} dy={'0.32em'}>
398
- {d < 0 ? '-' : ''}
399
- {valueFormatter(Math.abs(d), 'bytes_per_second', decimals)}
400
- </text>
401
- </g>
402
- <g key={`grid-${i}`}>
403
- <line
404
- className="stroke-gray-300 dark:stroke-gray-500"
405
- x1={xScale(from)}
406
- x2={xScale(to)}
407
- y1={yScale(d)}
408
- y2={yScale(d)}
409
- />
410
- </g>
411
- </Fragment>
412
- );
413
- })}
414
- <line
415
- className="stroke-gray-300 dark:stroke-gray-500"
416
- x1={0}
417
- x2={0}
418
- y1={0}
419
- y2={height - margin}
420
- />
421
- <line
422
- className="stroke-gray-300 dark:stroke-gray-500"
423
- x1={xScale(to)}
424
- x2={xScale(to)}
425
- y1={0}
426
- y2={height - margin}
427
- />
428
- <g transform={`translate(${-margin}, ${(height - margin) / 2}) rotate(270)`}>
429
- <text
430
- fill="currentColor"
431
- dy="-0.7em"
432
- className="text-sm capitalize"
433
- textAnchor="middle"
434
- >
435
- {humanReadableName}
436
- </text>
437
- </g>
438
- </g>
439
- <g
440
- className="x axis"
441
- fill="none"
442
- fontSize="10"
443
- textAnchor="middle"
444
- transform={`translate(0,${height - margin})`}
445
- >
446
- {xScale.ticks(5).map((d, i) => (
447
- <Fragment key={`${i.toString()}-${d.toString()}`}>
448
- <g
449
- key={`tick-${i}`}
450
- className="tick"
451
- /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
452
- transform={`translate(${xScale(d)}, 0)`}
453
- >
454
- <line y2={6} className="stroke-gray-300 dark:stroke-gray-500" />
455
- <text fill="currentColor" dy=".71em" y={9}>
456
- {formatDate(d, formatForTimespan(from, to), timezone)}
457
- </text>
458
- </g>
459
- <g key={`grid-${i}`}>
460
- <line
461
- className="stroke-gray-300 dark:stroke-gray-500"
462
- x1={xScale(d)}
463
- x2={xScale(d)}
464
- y1={0}
465
- y2={-height + margin}
466
- />
467
- </g>
468
- </Fragment>
469
- ))}
470
- <line
471
- className="stroke-gray-300 dark:stroke-gray-500"
472
- x1={0}
473
- x2={graphWidth}
474
- y1={0}
475
- y2={0}
476
- />
477
- <g transform={`translate(${(width - 2.5 * margin) / 2}, ${margin / 2})`}>
478
- <text fill="currentColor" dy=".71em" y={5} className="text-sm">
479
- Time
480
- </text>
481
- </g>
482
- </g>
483
- <g className="areas">
484
- {/* Draw baseline at y=0 */}
485
- <line
486
- x1={xScale(from)}
487
- x2={xScale(to)}
488
- y1={yScale(0)}
489
- y2={yScale(0)}
490
- stroke="#64748b"
491
- strokeDasharray="4 2"
492
- strokeWidth={1}
493
- opacity={0.7}
494
- />
495
- {series.map((s, i) => {
496
- let isSelected = false;
497
- if (parsedSelectedSeries != null && parsedSelectedSeries.length > 0) {
498
- isSelected = parsedSelectedSeries.every(m => {
499
- for (let i = 0; i < s.metric.length; i++) {
500
- if (s.metric[i].name === m.key && s.metric[i].value === m.value) {
501
- return true;
502
- }
503
- }
504
- return false;
505
- });
506
- }
507
-
508
- return (
509
- <g key={i} className="line cursor-pointer">
510
- <path
511
- d={lineGenerator(s.values) ?? ''}
512
- fill="none"
513
- stroke={getSeriesColor(s.metric)}
514
- strokeWidth={
515
- isSelected
516
- ? lineStrokeSelected
517
- : hovering && highlighted != null && i === highlighted.seriesIndex
518
- ? lineStrokeHover
519
- : lineStroke
520
- }
521
- strokeOpacity={isSelected ? 1 : 0.8}
522
- onClick={() => {
523
- if (highlighted != null && onSelectedSeriesChange != null) {
524
- onSelectedSeriesChange(
525
- highlighted.labels.map(l => ({
526
- key: l.name,
527
- value: l.value,
528
- }))
529
- );
530
- setSelectedTimeframe(undefined);
531
- }
532
- }}
533
- />
534
- </g>
535
- );
536
- })}
537
- </g>
538
- </g>
539
- </svg>
540
- </div>
541
- </>
255
+ <MetricsGraph
256
+ data={transformedData}
257
+ from={from}
258
+ to={to}
259
+ setTimeRange={setTimeRange}
260
+ onSampleClick={closestPoint => {
261
+ if (onSeriesClick != null) {
262
+ onSeriesClick(closestPoint.seriesIndex);
263
+ }
264
+ }}
265
+ yAxisLabel={humanReadableName}
266
+ yAxisUnit="bytes_per_second"
267
+ width={width}
268
+ height={height}
269
+ margin={margin}
270
+ contextMenuItems={contextMenuItems}
271
+ renderTooltipContent={(seriesIndex: number, pointIndex: number) => {
272
+ if (allOriginalData?.[seriesIndex]?.samples?.[pointIndex] != null) {
273
+ const originalSeriesData = allOriginalData[seriesIndex];
274
+ const originalPoint = allOriginalData[seriesIndex].samples[pointIndex];
275
+
276
+ const labels = originalSeriesData.labelset?.labels ?? [];
277
+ const nameLabel = labels.find(e => e.name === '__name__');
278
+ const highlightedNameLabel = nameLabel ?? {name: '', value: ''};
279
+
280
+ // Determine if this is receive data (negative values)
281
+ const isReceive = seriesIndex >= transmitData.length;
282
+ const valuePrefix = isReceive ? 'Receive ' : 'Transmit ';
283
+
284
+ return (
285
+ <div className="flex flex-row">
286
+ <div className="ml-2 mr-6">
287
+ <span className="font-semibold">{highlightedNameLabel.value}</span>
288
+ <span className="my-2 block text-gray-700 dark:text-gray-300">
289
+ <table className="table-auto">
290
+ <tbody>
291
+ <tr>
292
+ <td className="w-1/4">{valuePrefix}Value</td>
293
+ <td className="w-3/4">
294
+ {valueFormatter(Math.abs(originalPoint.value), 'bytes_per_second', 2)}
295
+ </td>
296
+ </tr>
297
+ <tr>
298
+ <td className="w-1/4">At</td>
299
+ <td className="w-3/4">
300
+ {formatDate(
301
+ new Date(originalPoint.timestamp),
302
+ timePattern(timezone as string),
303
+ timezone
304
+ )}
305
+ </td>
306
+ </tr>
307
+ </tbody>
308
+ </table>
309
+ </span>
310
+ <span className="my-2 block text-gray-500">
311
+ {labels
312
+ .filter(label => label.name !== '__name__')
313
+ .map(label => (
314
+ <div
315
+ key={`${seriesIndex.toString()}-${pointIndex.toString()}-${label.name}`}
316
+ className="mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-400"
317
+ >
318
+ <TextWithTooltip
319
+ text={`${transformUtilizationLabels(label.name)}="${label.value}"`}
320
+ maxTextLength={37}
321
+ id={`${seriesIndex.toString()}-${pointIndex.toString()}-tooltip-${
322
+ label.name
323
+ }`}
324
+ />
325
+ </div>
326
+ ))}
327
+ </span>
328
+ <div className="flex w-full items-center gap-1 text-xs text-gray-500">
329
+ <Icon icon="iconoir:mouse-button-right" />
330
+ <div>Right click to add labels to query.</div>
331
+ </div>
332
+ </div>
333
+ </div>
334
+ );
335
+ }
336
+ return null;
337
+ }}
338
+ />
542
339
  );
543
340
  };
544
341
 
@@ -553,11 +350,20 @@ const AreaChart = ({
553
350
  from,
554
351
  to,
555
352
  selectedSeries,
556
- onSelectedSeriesChange,
353
+ onSeriesClick,
557
354
  }: Props): JSX.Element => {
558
355
  const {isDarkMode} = useParcaContext();
559
356
  const {width, height, margin, heightStyle} = useMetricsGraphDimensions(false, true);
560
357
 
358
+ const transformedData = useMemo(
359
+ () => transformNetworkSeriesToSeries(transmitData, receiveData),
360
+ [transmitData, receiveData]
361
+ );
362
+
363
+ const contextMenuItems = useMemo(() => {
364
+ return createThroughputContextMenuItems(addLabelMatcher, transmitData, receiveData);
365
+ }, [addLabelMatcher, transmitData, receiveData]);
366
+
561
367
  return (
562
368
  <AnimatePresence>
563
369
  <motion.div
@@ -573,6 +379,7 @@ const AreaChart = ({
573
379
  <RawAreaChart
574
380
  transmitData={transmitData}
575
381
  receiveData={receiveData}
382
+ transformedData={transformedData}
576
383
  addLabelMatcher={addLabelMatcher}
577
384
  setTimeRange={setTimeRange}
578
385
  width={width}
@@ -583,7 +390,8 @@ const AreaChart = ({
583
390
  from={from}
584
391
  to={to}
585
392
  selectedSeries={selectedSeries}
586
- onSelectedSeriesChange={onSelectedSeriesChange}
393
+ onSeriesClick={onSeriesClick}
394
+ contextMenuItems={contextMenuItems}
587
395
  />
588
396
  )}
589
397
  </motion.div>