@parca/profile 0.16.0 → 0.16.22

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