Skip to content

Commit b5ce89a

Browse files
feat(webapp): Add tooltip in explore timeline (#1422)
1 parent a87c746 commit b5ce89a

File tree

15 files changed

+582
-93
lines changed

15 files changed

+582
-93
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React, { FC, useMemo } from 'react';
2+
import Color from 'color';
3+
import { getFormatter } from '@pyroscope/flamegraph/src/format/format';
4+
import { Profile } from '@pyroscope/models/src';
5+
import styles from './styles.module.scss';
6+
7+
export interface ExploreTooltipProps {
8+
timeLabel?: string;
9+
values?: Array<{
10+
closest: number[];
11+
color: number[];
12+
tagName: string;
13+
}>;
14+
profile?: Profile;
15+
}
16+
17+
const ExploreTooltip: FC<ExploreTooltipProps> = ({
18+
timeLabel,
19+
values,
20+
profile,
21+
}) => {
22+
const numTicks = profile?.flamebearer?.numTicks;
23+
const sampleRate = profile?.metadata?.sampleRate;
24+
const units = profile?.metadata?.units;
25+
26+
const formatter = useMemo(
27+
() =>
28+
numTicks &&
29+
typeof sampleRate === 'number' &&
30+
units &&
31+
getFormatter(numTicks, sampleRate, units),
32+
[numTicks, sampleRate, units]
33+
);
34+
35+
const total = useMemo(() => {
36+
if (numTicks && typeof sampleRate === 'number' && formatter) {
37+
return (
38+
parseFloat(formatter.format(numTicks, sampleRate).split(' ')?.[0]) || 0
39+
);
40+
}
41+
42+
return 0;
43+
}, [numTicks, sampleRate, formatter]);
44+
45+
const formatValue = (v: number) => {
46+
if (formatter && typeof sampleRate === 'number') {
47+
const value = formatter.format(v, sampleRate);
48+
const numberValue = parseFloat(value.split(' ')?.[0]) || 0;
49+
50+
const percent = (numberValue / total) * 100;
51+
52+
return `${value} (${percent.toFixed(2)}%)`;
53+
}
54+
55+
return 0;
56+
};
57+
58+
return (
59+
<div>
60+
<div className={styles.time}>{timeLabel}</div>
61+
{values?.length
62+
? values.map((v) => {
63+
return (
64+
<div key={v?.tagName} className={styles.valueWrapper}>
65+
<div
66+
className={styles.valueColor}
67+
style={{
68+
backgroundColor: Color.rgb(v.color).toString(),
69+
}}
70+
/>
71+
<div>{v.tagName}:</div>
72+
<div className={styles.closest}>
73+
{formatValue(v?.closest?.[1] || 0)}
74+
</div>
75+
</div>
76+
);
77+
})
78+
: null}
79+
</div>
80+
);
81+
};
82+
83+
export default ExploreTooltip;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
.time {
2+
font-size: 14px;
3+
}
4+
5+
.valueWrapper {
6+
display: flex;
7+
align-items: center;
8+
flex-direction: row;
9+
font-size: 14px;
10+
}
11+
12+
.valueColor {
13+
width: 20px;
14+
height: 6px;
15+
border-radius: 3px;
16+
margin-right: 8px;
17+
}
18+
19+
.closest {
20+
font-weight: bold;
21+
margin-left: 8px;
22+
}

webapp/javascript/components/TimelineChart/TimelineChartSelection.ts renamed to webapp/javascript/components/TimelineChart/Selection.plugin.ts

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,7 @@
11
/* eslint-disable */
22
// extending logic of Flot's selection plugin (react-flot/flot/jquery.flot.selection)
3-
4-
type PlotType = {
5-
getPlotOffset: () => any;
6-
getOptions: () => any;
7-
getAxes: () => any[];
8-
getXAxes: () => any[];
9-
getYAxes: () => any[];
10-
getPlaceholder: () => {
11-
trigger: (arg0: string, arg1: any[]) => void;
12-
offset: () => {
13-
left: number;
14-
top: number;
15-
};
16-
};
17-
triggerRedrawOverlay: () => void;
18-
width: () => number;
19-
height: () => number;
20-
clearSelection: (preventEvent: boolean) => void;
21-
setSelection: (ranges: any, preventEvent: any) => void;
22-
getSelection: () => {} | null;
23-
hooks: any;
24-
};
25-
26-
type CtxType = {
27-
save: () => void;
28-
translate: (arg0: any, arg1: any) => void;
29-
strokeStyle: any;
30-
lineWidth: number;
31-
lineJoin: any;
32-
fillStyle: any;
33-
fillRect: (arg0: number, arg1: number, arg2: number, arg3: number) => void;
34-
strokeRect: (arg0: number, arg1: number, arg2: number, arg3: number) => void;
35-
restore: () => void;
36-
};
37-
38-
type EventHolderType = {
39-
unbind: (arg0: string, arg1: { (e: any): void; (e: any): void }) => void;
40-
mousemove: (arg0: (e: EventType) => void) => void;
41-
mousedown: (arg0: (e: EventType) => void) => void;
42-
};
43-
44-
type EventType = { pageX: number; pageY: number; which?: number };
3+
import { PlotType, CtxType, EventHolderType, EventType } from './types';
4+
import clamp from './clamp';
455

466
const handleWidth = 4;
477
const handleHeight = 22;
@@ -77,7 +37,7 @@ const handleHeight = 22;
7737
function getCursorPositionX(e: EventType) {
7838
const plotOffset = plot.getPlotOffset();
7939
const offset = plot.getPlaceholder().offset();
80-
return clamp(0, e.pageX - offset.left - plotOffset.left, plot.width());
40+
return clamp(0, plot.width(), e.pageX - offset.left - plotOffset.left);
8141
}
8242

8343
function getPlotSelection() {
@@ -274,16 +234,12 @@ const handleHeight = 22;
274234
]);
275235
}
276236

277-
function clamp(min: number, value: number, max: number) {
278-
return value < min ? min : value > max ? max : value;
279-
}
280-
281237
function setSelectionPos(pos: { x: number; y: number }, e: EventType) {
282238
var o = plot.getOptions();
283239
var offset = plot.getPlaceholder().offset();
284240
var plotOffset = plot.getPlotOffset();
285-
pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width());
286-
pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height());
241+
pos.x = clamp(0, plot.width(), e.pageX - offset.left - plotOffset.left);
242+
pos.y = clamp(0, plot.height(), e.pageY - offset.top - plotOffset.top);
287243

288244
if (o.selection.mode == 'y')
289245
pos.x = pos == selection.first ? 0 : plot.width();

webapp/javascript/components/TimelineChart/TimelineChart.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import React from 'react';
44

55
import ReactFlot from 'react-flot';
66
import 'react-flot/flot/jquery.flot.time.min';
7-
import './TimelineChartSelection';
7+
import './Selection.plugin';
88
import 'react-flot/flot/jquery.flot.crosshair.min';
99
import './TimelineChartPlugin';
10+
import './Tooltip.plugin';
1011

1112
interface TimelineChartProps {
1213
onSelect: (from: string, until: string) => void;

webapp/javascript/components/TimelineChart/TimelineChartPlugin.ts

Lines changed: 36 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
22
// @ts-nocheck
3-
import { format } from 'date-fns';
4-
import { getUTCdate } from '@webapp/util/formatDate';
3+
import getFormatLabel from './getFormatLabel';
54

65
(function ($) {
76
const options = {}; // no options
@@ -24,51 +23,28 @@ import { getUTCdate } from '@webapp/util/formatDate';
2423
width: 0,
2524
};
2625

27-
function getFormatLabel(date) {
28-
// Format labels in accordance with xaxis tick size
26+
const onPlotHover = (target, position) => {
2927
const { xaxis } = plot.getAxes();
3028

31-
if (!xaxis) {
32-
return '';
33-
}
34-
35-
try {
36-
const d = getUTCdate(
37-
new Date(date),
38-
plotOptions.xaxis.timezone === 'utc'
39-
? 0
40-
: new Date().getTimezoneOffset()
41-
);
42-
43-
const hours = Math.abs(xaxis.max - xaxis.min) / 60 / 60 / 1000;
44-
45-
if (hours < 12) {
46-
return format(d, 'HH:mm:ss');
47-
}
48-
if (hours > 12 && hours < 24) {
49-
return format(d, 'HH:mm');
50-
}
51-
if (hours > 24) {
52-
return format(d, 'MMM do HH:mm');
53-
}
54-
return format(d, 'MMM do HH:mm');
55-
} catch (e) {
56-
return '???';
57-
}
58-
}
59-
60-
const onPlotHover = (target, position) => {
6129
this.tooltipY = target.currentTarget.getBoundingClientRect().bottom - 28;
6230
if (!position.x) return;
6331
if (!this.selecting) {
6432
this.selectingFrom = {
65-
label: getFormatLabel(position.x),
33+
label: getFormatLabel({
34+
date: position.x,
35+
xaxis,
36+
timezone: plotOptions.xaxis.timezone,
37+
}),
6638
x: position.x,
6739
pageX: position.pageX,
6840
};
6941
} else {
7042
this.selectingTo = {
71-
label: getFormatLabel(position.x),
43+
label: getFormatLabel({
44+
date: position.x,
45+
xaxis,
46+
timezone: plotOptions.xaxis.timezone,
47+
}),
7248
x: position.x,
7349
pageX: position.pageX,
7450
};
@@ -77,6 +53,8 @@ import { getUTCdate } from '@webapp/util/formatDate';
7753
};
7854

7955
const updateTooltips = () => {
56+
const { xaxis } = plot.getAxes();
57+
8058
if (!this.selecting) {
8159
// If we arn't in selection mode
8260
this.$tooltip.html(this.selectingFrom.label).show();
@@ -88,12 +66,16 @@ import { getUTCdate } from '@webapp/util/formatDate';
8866
} else {
8967
// Render Intersection
9068
this.$tooltip.html(
91-
`${getFormatLabel(
92-
Math.min(this.selectingFrom.x, this.selectingTo.x)
93-
)} -
94-
${getFormatLabel(
95-
Math.max(this.selectingFrom.x, this.selectingTo.x)
96-
)}`
69+
`${getFormatLabel({
70+
date: Math.min(this.selectingFrom.x, this.selectingTo.x),
71+
xaxis,
72+
timezone: plotOptions.xaxis.timezone,
73+
})} -
74+
${getFormatLabel({
75+
date: Math.max(this.selectingFrom.x, this.selectingTo.x),
76+
xaxis,
77+
timezone: plotOptions.xaxis.timezone,
78+
})}`
9779
);
9880

9981
// Stick to left selection
@@ -180,6 +162,12 @@ import { getUTCdate } from '@webapp/util/formatDate';
180162
};
181163

182164
function bindEvents(plot, eventHolder) {
165+
const o = plot.getOptions();
166+
167+
if (o.onHoverDisplayTooltip) {
168+
return;
169+
}
170+
183171
plot.getPlaceholder().bind('plothover', onPlotHover);
184172
plot.getPlaceholder().bind('plotselected', onSelected);
185173

@@ -192,6 +180,12 @@ import { getUTCdate } from '@webapp/util/formatDate';
192180
}
193181

194182
function shutdown(plot, eventHolder) {
183+
const o = plot.getOptions();
184+
185+
if (o.onHoverDisplayTooltip) {
186+
return;
187+
}
188+
195189
plot.getPlaceholder().unbind('plothover', onPlotHover);
196190
// plot.getPlaceholder().unbind('plotselecting', onSelecting);
197191
plot.getPlaceholder().unbind('plotselected', onSelected);

0 commit comments

Comments
 (0)