@parca/profile 0.16.0 → 0.16.43

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 (98) hide show
  1. package/CHANGELOG.md +52 -25
  2. package/dist/Callgraph/Edge/index.d.ts +23 -0
  3. package/dist/Callgraph/Edge/index.js +30 -0
  4. package/dist/Callgraph/Node/index.d.ts +20 -0
  5. package/dist/Callgraph/Node/index.js +37 -0
  6. package/dist/Callgraph/index.d.ts +9 -0
  7. package/dist/Callgraph/index.js +137 -0
  8. package/dist/Callgraph/mockData/index.d.ts +148 -0
  9. package/dist/Callgraph/mockData/index.js +577 -0
  10. package/dist/Callgraph/utils.d.ts +19 -0
  11. package/dist/Callgraph/utils.js +82 -0
  12. package/dist/GraphTooltip/index.d.ts +20 -0
  13. package/dist/GraphTooltip/index.js +155 -0
  14. package/dist/IcicleGraph.d.ts +36 -0
  15. package/dist/IcicleGraph.js +158 -0
  16. package/dist/MatchersInput/index.d.ts +24 -0
  17. package/dist/MatchersInput/index.js +479 -0
  18. package/dist/MetricsCircle/index.d.ts +8 -0
  19. package/dist/MetricsCircle/index.js +18 -0
  20. package/dist/MetricsGraph/index.d.ts +36 -0
  21. package/dist/MetricsGraph/index.js +327 -0
  22. package/dist/MetricsSeries/index.d.ts +12 -0
  23. package/dist/MetricsSeries/index.js +21 -0
  24. package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts +20 -0
  25. package/dist/ProfileExplorer/ProfileExplorerCompare.js +38 -0
  26. package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts +16 -0
  27. package/dist/ProfileExplorer/ProfileExplorerSingle.js +19 -0
  28. package/dist/ProfileExplorer/index.d.ts +10 -0
  29. package/dist/ProfileExplorer/index.js +203 -0
  30. package/dist/ProfileIcicleGraph.d.ts +11 -0
  31. package/dist/ProfileIcicleGraph.js +28 -0
  32. package/dist/ProfileMetricsGraph/index.d.ts +23 -0
  33. package/dist/ProfileMetricsGraph/index.js +127 -0
  34. package/dist/ProfileSVG.module.css +3 -0
  35. package/dist/ProfileSelector/CompareButton.d.ts +6 -0
  36. package/dist/ProfileSelector/CompareButton.js +41 -0
  37. package/dist/ProfileSelector/MergeButton.d.ts +6 -0
  38. package/dist/ProfileSelector/MergeButton.js +41 -0
  39. package/dist/ProfileSelector/index.d.ts +30 -0
  40. package/dist/ProfileSelector/index.js +133 -0
  41. package/dist/ProfileSource.d.ts +89 -0
  42. package/dist/ProfileSource.js +239 -0
  43. package/dist/ProfileTypeSelector/index.d.ts +21 -0
  44. package/dist/ProfileTypeSelector/index.js +138 -0
  45. package/dist/ProfileView.d.ts +40 -0
  46. package/dist/ProfileView.js +111 -0
  47. package/dist/ProfileView.styles.css +3 -0
  48. package/dist/ProfileViewWithData.d.ts +12 -0
  49. package/dist/ProfileViewWithData.js +116 -0
  50. package/dist/TopTable.d.ts +10 -0
  51. package/dist/TopTable.js +140 -0
  52. package/dist/TopTable.styles.css +7 -0
  53. package/dist/components/DiffLegend.d.ts +3 -0
  54. package/dist/components/DiffLegend.js +62 -0
  55. package/dist/components/ProfileShareButton/ResultBox.d.ts +7 -0
  56. package/dist/components/ProfileShareButton/ResultBox.js +46 -0
  57. package/dist/components/ProfileShareButton/index.d.ts +8 -0
  58. package/dist/components/ProfileShareButton/index.js +119 -0
  59. package/dist/index.d.ts +13 -0
  60. package/dist/index.js +64 -0
  61. package/dist/stories/ProfileTypeSelector.stories.d.ts +5 -0
  62. package/dist/stories/ProfileTypeSelector.stories.js +77 -0
  63. package/dist/stories/ProfileView.stories.d.ts +5 -0
  64. package/dist/stories/ProfileView.stories.js +22 -0
  65. package/dist/stories/mockdata/flamegraphData.json +7960 -0
  66. package/dist/styles.css +1 -0
  67. package/dist/useDelayedLoader.d.ts +5 -0
  68. package/dist/useDelayedLoader.js +33 -0
  69. package/dist/useQuery.d.ts +13 -0
  70. package/dist/useQuery.js +41 -0
  71. package/dist/utils.d.ts +4 -0
  72. package/dist/utils.js +83 -0
  73. package/package.json +13 -8
  74. package/src/Callgraph/Edge/index.tsx +59 -0
  75. package/src/Callgraph/Node/index.tsx +66 -0
  76. package/src/Callgraph/index.tsx +169 -0
  77. package/src/Callgraph/mockData/index.ts +605 -0
  78. package/src/Callgraph/utils.ts +116 -0
  79. package/src/GraphTooltip/index.tsx +340 -0
  80. package/src/IcicleGraph.tsx +28 -8
  81. package/src/MatchersInput/index.tsx +698 -0
  82. package/src/MetricsCircle/index.tsx +28 -0
  83. package/src/MetricsGraph/index.tsx +589 -0
  84. package/src/MetricsSeries/index.tsx +38 -0
  85. package/src/ProfileExplorer/ProfileExplorerCompare.tsx +109 -0
  86. package/src/ProfileExplorer/ProfileExplorerSingle.tsx +72 -0
  87. package/src/ProfileExplorer/index.tsx +377 -0
  88. package/src/ProfileMetricsGraph/index.tsx +143 -0
  89. package/src/ProfileSelector/CompareButton.tsx +72 -0
  90. package/src/ProfileSelector/MergeButton.tsx +72 -0
  91. package/src/ProfileSelector/index.tsx +270 -0
  92. package/src/ProfileTypeSelector/index.tsx +180 -0
  93. package/src/ProfileView.tsx +2 -7
  94. package/src/index.tsx +11 -0
  95. package/src/useQuery.tsx +1 -0
  96. package/tailwind.config.js +8 -0
  97. package/tsconfig.json +7 -3
  98. package/typings.d.ts +14 -0
@@ -0,0 +1,28 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+
14
+ interface MetricsCircleProps {
15
+ cx: number;
16
+ cy: number;
17
+ radius?: number;
18
+ }
19
+
20
+ const defaultRadius = 3;
21
+
22
+ const MetricsCircle = ({cx, cy, radius}: MetricsCircleProps): JSX.Element => (
23
+ <g className="circle">
24
+ <circle cx={cx} cy={cy} r={radius !== undefined ? radius : defaultRadius}></circle>
25
+ </g>
26
+ );
27
+
28
+ export default MetricsCircle;
@@ -0,0 +1,589 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+
14
+ import React, {useEffect, useRef, useState} from 'react';
15
+ import * as d3 from 'd3';
16
+ import {pointer} from 'd3-selection';
17
+ import {formatForTimespan} from '@parca/functions/time';
18
+ import {SingleProfileSelection, timeFormat} from '..';
19
+ import {cutToMaxStringLength} from '@parca/functions/string';
20
+ import throttle from 'lodash.throttle';
21
+ import {MetricsSeries as MetricsSeriesPb, MetricsSample, Label} from '@parca/client';
22
+ import {usePopper} from 'react-popper';
23
+ import type {VirtualElement} from '@popperjs/core';
24
+ import {valueFormatter, formatDate} from '@parca/functions';
25
+ import {DateTimeRange} from '@parca/components';
26
+ import {useContainerDimensions} from '@parca/dynamicsize';
27
+ import useIsShiftDown from '@parca/components/src/hooks/useIsShiftDown';
28
+
29
+ import MetricsSeries from '../MetricsSeries';
30
+ import MetricsCircle from '../MetricsCircle';
31
+
32
+ interface RawMetricsGraphProps {
33
+ data: MetricsSeriesPb[];
34
+ from: number;
35
+ to: number;
36
+ profile: SingleProfileSelection | null;
37
+ onSampleClick: (timestamp: number, value: number, labels: Label[]) => void;
38
+ onLabelClick: (labelName: string, labelValue: string) => void;
39
+ setTimeRange: (range: DateTimeRange) => void;
40
+ sampleUnit: string;
41
+ width?: number;
42
+ }
43
+
44
+ interface HighlightedSeries {
45
+ seriesIndex: number;
46
+ labels: Label[];
47
+ timestamp: number;
48
+ value: number;
49
+ x: number;
50
+ y: number;
51
+ }
52
+
53
+ interface Series {
54
+ metric: Label[];
55
+ values: number[][];
56
+ }
57
+
58
+ const MetricsGraph = ({
59
+ data,
60
+ from,
61
+ to,
62
+ profile,
63
+ onSampleClick,
64
+ onLabelClick,
65
+ setTimeRange,
66
+ sampleUnit,
67
+ }: RawMetricsGraphProps): JSX.Element => {
68
+ const {ref, dimensions} = useContainerDimensions();
69
+
70
+ return (
71
+ <div ref={ref}>
72
+ <RawMetricsGraph
73
+ data={data}
74
+ from={from}
75
+ to={to}
76
+ profile={profile}
77
+ onSampleClick={onSampleClick}
78
+ onLabelClick={onLabelClick}
79
+ setTimeRange={setTimeRange}
80
+ sampleUnit={sampleUnit}
81
+ width={dimensions?.width}
82
+ />
83
+ </div>
84
+ );
85
+ };
86
+
87
+ export default MetricsGraph;
88
+
89
+ export const parseValue = (value: string): number | null => {
90
+ const val = parseFloat(value);
91
+ // "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They
92
+ // can't be graphed, so show them as gaps (null).
93
+ return isNaN(val) ? null : val;
94
+ };
95
+
96
+ const lineStroke = '1px';
97
+ const lineStrokeHover = '2px';
98
+
99
+ interface MetricsTooltipProps {
100
+ x: number;
101
+ y: number;
102
+ highlighted: HighlightedSeries;
103
+ onLabelClick: (labelName: string, labelValue: string) => void;
104
+ contextElement: Element | null;
105
+ sampleUnit: string;
106
+ }
107
+
108
+ function generateGetBoundingClientRect(contextElement: Element, x = 0, y = 0): () => DOMRect {
109
+ const domRect = contextElement.getBoundingClientRect();
110
+ return () =>
111
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
112
+ ({
113
+ width: 0,
114
+ height: 0,
115
+ top: domRect.y + y,
116
+ left: domRect.x + x,
117
+ right: domRect.x + x,
118
+ bottom: domRect.y + y,
119
+ } as DOMRect);
120
+ }
121
+
122
+ const virtualElement: VirtualElement = {
123
+ getBoundingClientRect: () => {
124
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
125
+ return {
126
+ width: 0,
127
+ height: 0,
128
+ top: 0,
129
+ left: 0,
130
+ right: 0,
131
+ bottom: 0,
132
+ } as DOMRect;
133
+ },
134
+ };
135
+
136
+ export const MetricsTooltip = ({
137
+ x,
138
+ y,
139
+ highlighted,
140
+ onLabelClick,
141
+ contextElement,
142
+ sampleUnit,
143
+ }: MetricsTooltipProps): JSX.Element => {
144
+ const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
145
+
146
+ const {styles, attributes, ...popperProps} = usePopper(virtualElement, popperElement, {
147
+ placement: 'auto-start',
148
+ strategy: 'absolute',
149
+ modifiers: [
150
+ {
151
+ name: 'preventOverflow',
152
+ options: {
153
+ tether: false,
154
+ altAxis: true,
155
+ },
156
+ },
157
+ {
158
+ name: 'offset',
159
+ options: {
160
+ offset: [30, 30],
161
+ },
162
+ },
163
+ ],
164
+ });
165
+
166
+ const update = popperProps.update;
167
+
168
+ useEffect(() => {
169
+ if (contextElement != null) {
170
+ virtualElement.getBoundingClientRect = generateGetBoundingClientRect(contextElement, x, y);
171
+ void update?.();
172
+ }
173
+ }, [x, y, contextElement, update]);
174
+
175
+ const nameLabel: Label | undefined = highlighted?.labels.find(e => e.name === '__name__');
176
+ const highlightedNameLabel: Label = nameLabel !== undefined ? nameLabel : {name: '', value: ''};
177
+
178
+ return (
179
+ <div ref={setPopperElement} style={styles.popper} {...attributes.popper} className="z-10">
180
+ <div className="flex max-w-md">
181
+ <div className="m-auto">
182
+ <div
183
+ className="border-gray-300 dark:border-gray-500 bg-gray-50 dark:bg-gray-900 rounded-lg p-3 shadow-lg opacity-90"
184
+ style={{borderWidth: 1}}
185
+ >
186
+ <div className="flex flex-row">
187
+ <div className="ml-2 mr-6">
188
+ <span className="font-semibold">{highlightedNameLabel.value}</span>
189
+ <span className="block text-gray-700 dark:text-gray-300 my-2">
190
+ <table className="table-auto">
191
+ <tbody>
192
+ <tr>
193
+ <td className="w-1/4">Value</td>
194
+ <td className="w-3/4">
195
+ {valueFormatter(highlighted.value, sampleUnit, 1)}
196
+ </td>
197
+ </tr>
198
+ <tr>
199
+ <td className="w-1/4">At</td>
200
+ <td className="w-3/4">{formatDate(highlighted.timestamp, timeFormat)}</td>
201
+ </tr>
202
+ </tbody>
203
+ </table>
204
+ </span>
205
+ <span className="block text-gray-500 my-2">
206
+ {highlighted.labels
207
+ .filter((label: Label) => label.name !== '__name__')
208
+ .map(function (label: Label) {
209
+ return (
210
+ <button
211
+ key={label.name}
212
+ type="button"
213
+ className="inline-block rounded-lg text-gray-700 bg-gray-200 dark:bg-gray-700 dark:text-gray-400 px-2 py-1 text-xs font-bold mr-3"
214
+ onClick={() => onLabelClick(label.name, label.value)}
215
+ >
216
+ {cutToMaxStringLength(`${label.name}="${label.value}"`, 37)}
217
+ </button>
218
+ );
219
+ })}
220
+ </span>
221
+ <span className="block text-gray-500 text-xs">
222
+ Hold shift and click label to add to query.
223
+ </span>
224
+ </div>
225
+ </div>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ );
231
+ };
232
+
233
+ export const RawMetricsGraph = ({
234
+ data,
235
+ from,
236
+ to,
237
+ profile,
238
+ onSampleClick,
239
+ onLabelClick,
240
+ setTimeRange,
241
+ width,
242
+ sampleUnit,
243
+ }: RawMetricsGraphProps): JSX.Element => {
244
+ const graph = useRef(null);
245
+ const [dragging, setDragging] = useState(false);
246
+ const [hovering, setHovering] = useState(false);
247
+ const [relPos, setRelPos] = useState(-1);
248
+ const [pos, setPos] = useState([0, 0]);
249
+ const metricPointRef = useRef(null);
250
+ const isShiftDown = useIsShiftDown();
251
+
252
+ const time: number = parseFloat(profile?.HistoryParams().time);
253
+
254
+ if (width === undefined || width == null) {
255
+ width = 0;
256
+ }
257
+
258
+ const height = Math.min(width / 2.5, 400);
259
+ const margin = 50;
260
+ const marginRight = 20;
261
+
262
+ const series: Series[] = data.reduce<Series[]>(function (agg: Series[], s: MetricsSeriesPb) {
263
+ if (s.labelset !== undefined) {
264
+ agg.push({
265
+ metric: s.labelset.labels,
266
+ values: s.samples.reduce<number[][]>(function (agg: number[][], d: MetricsSample) {
267
+ if (d.timestamp !== undefined && d.value !== undefined) {
268
+ const t = (+d.timestamp.seconds * 1e9 + d.timestamp.nanos) / 1e6; // https://github.com/microsoft/TypeScript/issues/5710#issuecomment-157886246
269
+ agg.push([t, parseFloat(d.value)]);
270
+ }
271
+ return agg;
272
+ }, []),
273
+ });
274
+ }
275
+ return agg;
276
+ }, []);
277
+
278
+ const extentsY = series.map(function (s) {
279
+ return d3.extent(s.values, function (d) {
280
+ return d[1];
281
+ });
282
+ });
283
+
284
+ const minY = d3.min(extentsY, function (d) {
285
+ return d[0];
286
+ });
287
+ const maxY = d3.max(extentsY, function (d) {
288
+ return d[1];
289
+ });
290
+
291
+ /* Scale */
292
+ const xScale = d3
293
+ .scaleUtc()
294
+ .domain([from, to])
295
+ .range([0, width - margin - marginRight]);
296
+
297
+ const yScale = d3
298
+ .scaleLinear()
299
+ // tslint:disable-next-line
300
+ .domain([minY, maxY] as Iterable<d3.NumberValue>)
301
+ .range([height - margin, 0]);
302
+
303
+ const color = d3.scaleOrdinal(d3.schemeCategory10);
304
+
305
+ const l = d3.line(
306
+ d => xScale(d[0]),
307
+ d => yScale(d[1])
308
+ );
309
+
310
+ const getClosest = (): HighlightedSeries | null => {
311
+ const closestPointPerSeries = series.map(function (s) {
312
+ const distances = s.values.map(d => {
313
+ const x = xScale(d[0]);
314
+ const y = yScale(d[1]);
315
+
316
+ return Math.sqrt(Math.pow(pos[0] - x, 2) + Math.pow(pos[1] - y, 2));
317
+ });
318
+
319
+ const pointIndex = d3.minIndex(distances);
320
+ const minDistance = distances[pointIndex];
321
+ return {
322
+ pointIndex,
323
+ distance: minDistance,
324
+ };
325
+ });
326
+
327
+ const closestSeriesIndex = d3.minIndex(closestPointPerSeries, s => s.distance);
328
+ const pointIndex = closestPointPerSeries[closestSeriesIndex].pointIndex;
329
+ const point = series[closestSeriesIndex].values[pointIndex];
330
+
331
+ return {
332
+ seriesIndex: closestSeriesIndex,
333
+ labels: series[closestSeriesIndex].metric,
334
+ timestamp: point[0],
335
+ value: point[1],
336
+ x: xScale(point[0]),
337
+ y: yScale(point[1]),
338
+ };
339
+ };
340
+
341
+ const highlighted = getClosest();
342
+
343
+ const onMouseDown = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
344
+ // only left mouse button
345
+ if (e.button !== 0) {
346
+ return;
347
+ }
348
+
349
+ // X/Y coordinate array relative to svg
350
+ const rel = pointer(e);
351
+
352
+ const xCoordinate = rel[0];
353
+ const xCoordinateWithoutMargin = xCoordinate - margin;
354
+ if (xCoordinateWithoutMargin >= 0) {
355
+ setRelPos(xCoordinateWithoutMargin);
356
+ setDragging(true);
357
+ }
358
+
359
+ e.stopPropagation();
360
+ e.preventDefault();
361
+ };
362
+
363
+ const openClosestProfile = (): void => {
364
+ if (highlighted != null) {
365
+ onSampleClick(Math.round(highlighted.timestamp), highlighted.value, highlighted.labels);
366
+ }
367
+ };
368
+
369
+ const onMouseUp = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
370
+ setDragging(false);
371
+
372
+ if (relPos === -1) {
373
+ // MouseDown happened outside of this element.
374
+ return;
375
+ }
376
+
377
+ // This is a normal click. We tolerate tiny movements to still be a
378
+ // click as they can occur when clicking based on user feedback.
379
+ if (Math.abs(relPos - pos[0]) <= 1) {
380
+ openClosestProfile();
381
+ setRelPos(-1);
382
+ return;
383
+ }
384
+
385
+ const firstTime = xScale.invert(relPos).valueOf();
386
+ const secondTime = xScale.invert(pos[0]).valueOf();
387
+
388
+ if (firstTime > secondTime) {
389
+ setTimeRange(DateTimeRange.fromAbsoluteDates(secondTime, firstTime));
390
+ } else {
391
+ setTimeRange(DateTimeRange.fromAbsoluteDates(firstTime, secondTime));
392
+ }
393
+ setRelPos(-1);
394
+
395
+ e.stopPropagation();
396
+ e.preventDefault();
397
+ };
398
+
399
+ const throttledSetPos = throttle(setPos, 20);
400
+
401
+ const onMouseMove = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
402
+ // X/Y coordinate array relative to svg
403
+ const rel = pointer(e);
404
+
405
+ const xCoordinate = rel[0];
406
+ const xCoordinateWithoutMargin = xCoordinate - margin;
407
+ const yCoordinate = rel[1];
408
+ const yCoordinateWithoutMargin = yCoordinate - margin;
409
+
410
+ if (!isShiftDown) {
411
+ throttledSetPos([xCoordinateWithoutMargin, yCoordinateWithoutMargin]);
412
+ }
413
+ };
414
+
415
+ const findSelectedProfile = (): HighlightedSeries | null => {
416
+ if (profile == null) {
417
+ return null;
418
+ }
419
+
420
+ let s: Series | null = null;
421
+ let seriesIndex: number = -1;
422
+
423
+ outer: for (let i = 0; i < series.length; i++) {
424
+ const keys = profile.labels.map(e => e.name);
425
+ for (let j = 0; j < keys.length; j++) {
426
+ const labelName = keys[j];
427
+ const label = series[i].metric.find(e => e.name === labelName);
428
+ if (label === undefined) {
429
+ continue outer; // label doesn't exist to begin with
430
+ }
431
+ if (profile.labels[j].value !== label.value) {
432
+ continue outer; // label values don't match
433
+ }
434
+ }
435
+ seriesIndex = i;
436
+ s = series[i];
437
+ }
438
+
439
+ if (s == null) {
440
+ return null;
441
+ }
442
+ // Find the sample that matches the timestamp
443
+ const sample = s.values.find(v => {
444
+ return Math.round(v[0]) === time;
445
+ });
446
+ if (sample === undefined) {
447
+ return null;
448
+ }
449
+
450
+ return {
451
+ labels: [],
452
+ seriesIndex,
453
+ timestamp: sample[0],
454
+ value: sample[1],
455
+ x: xScale(sample[0]),
456
+ y: yScale(sample[1]),
457
+ };
458
+ };
459
+
460
+ const selected = findSelectedProfile();
461
+
462
+ return (
463
+ <>
464
+ {highlighted != null && hovering && !dragging && pos[0] !== 0 && pos[1] !== 0 && (
465
+ <div
466
+ onMouseMove={onMouseMove}
467
+ onMouseEnter={() => setHovering(true)}
468
+ onMouseLeave={() => setHovering(false)}
469
+ >
470
+ <MetricsTooltip
471
+ x={pos[0] + margin}
472
+ y={pos[1] + margin}
473
+ highlighted={highlighted}
474
+ onLabelClick={onLabelClick}
475
+ contextElement={graph.current}
476
+ sampleUnit={sampleUnit}
477
+ />
478
+ </div>
479
+ )}
480
+ <div
481
+ ref={graph}
482
+ onMouseEnter={function () {
483
+ setHovering(true);
484
+ }}
485
+ onMouseLeave={() => setHovering(false)}
486
+ >
487
+ <svg
488
+ width={`${width}px`}
489
+ height={`${height + margin}px`}
490
+ onMouseDown={onMouseDown}
491
+ onMouseUp={onMouseUp}
492
+ onMouseMove={onMouseMove}
493
+ >
494
+ <g transform={`translate(${margin}, 0)`}>
495
+ {dragging && (
496
+ <g className="zoom-time-rect">
497
+ <rect
498
+ className="bar"
499
+ x={pos[0] - relPos < 0 ? pos[0] : relPos}
500
+ y={0}
501
+ height={height}
502
+ width={Math.abs(pos[0] - relPos)}
503
+ fill={'rgba(0, 0, 0, 0.125)'}
504
+ />
505
+ </g>
506
+ )}
507
+ </g>
508
+ <g transform={`translate(${margin}, ${margin})`}>
509
+ <g className="lines fill-transparent">
510
+ {series.map((s, i) => (
511
+ <g key={i} className="line">
512
+ <MetricsSeries
513
+ data={s}
514
+ line={l}
515
+ color={color(i.toString())}
516
+ strokeWidth={
517
+ hovering && highlighted != null && i === highlighted.seriesIndex
518
+ ? lineStrokeHover
519
+ : lineStroke
520
+ }
521
+ xScale={xScale}
522
+ yScale={yScale}
523
+ />
524
+ </g>
525
+ ))}
526
+ </g>
527
+ {hovering && highlighted != null && (
528
+ <g
529
+ className="circle-group"
530
+ ref={metricPointRef}
531
+ style={{fill: color(highlighted.seriesIndex.toString())}}
532
+ >
533
+ <MetricsCircle cx={highlighted.x} cy={highlighted.y} />
534
+ </g>
535
+ )}
536
+ {selected != null && (
537
+ <g
538
+ className="circle-group"
539
+ style={
540
+ selected?.seriesIndex != null
541
+ ? {fill: color(selected.seriesIndex.toString())}
542
+ : {}
543
+ }
544
+ >
545
+ <MetricsCircle cx={selected.x} cy={selected.y} radius={5} />
546
+ </g>
547
+ )}
548
+ <g
549
+ className="x axis"
550
+ fill="none"
551
+ fontSize="10"
552
+ textAnchor="middle"
553
+ transform={`translate(0,${height - margin})`}
554
+ >
555
+ {xScale.ticks(5).map((d, i) => (
556
+ <g
557
+ key={i}
558
+ className="tick"
559
+ /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
560
+ transform={`translate(${xScale(d)}, 0)`}
561
+ >
562
+ <line y2={6} stroke="currentColor" />
563
+ <text fill="currentColor" dy=".71em" y={9}>
564
+ {formatDate(d, formatForTimespan(from, to))}
565
+ </text>
566
+ </g>
567
+ ))}
568
+ </g>
569
+ <g className="y axis" textAnchor="end" fontSize="10" fill="none">
570
+ {yScale.ticks(3).map((d, i) => (
571
+ <g
572
+ key={i}
573
+ className="tick"
574
+ /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
575
+ transform={`translate(0, ${yScale(d)})`}
576
+ >
577
+ <line stroke="currentColor" x2={-6} />
578
+ <text fill="currentColor" x={-9} dy={'0.32em'}>
579
+ {valueFormatter(d, sampleUnit, 1)}
580
+ </text>
581
+ </g>
582
+ ))}
583
+ </g>
584
+ </g>
585
+ </svg>
586
+ </div>
587
+ </>
588
+ );
589
+ };
@@ -0,0 +1,38 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+
14
+ import * as d3 from 'd3';
15
+
16
+ interface MetricsSeriesProps {
17
+ data: any;
18
+ line: d3.Line<[number, number]>;
19
+ color: string;
20
+ strokeWidth: string;
21
+ xScale: (input: number) => number;
22
+ yScale: (input: number) => number;
23
+ }
24
+
25
+ const MetricsSeries = ({data, line, color, strokeWidth}: MetricsSeriesProps): JSX.Element => (
26
+ <g className="line-group">
27
+ <path
28
+ className="line"
29
+ d={line(data.values) ?? undefined}
30
+ style={{
31
+ stroke: color,
32
+ strokeWidth,
33
+ }}
34
+ />
35
+ </g>
36
+ );
37
+
38
+ export default MetricsSeries;