3
import { escapeHTML } from '../../utils';
4
import { GraphData, GraphExemplar, GraphProps, GraphSeries } from './Graph';
5
import moment from 'moment-timezone';
6
import { colorPool } from './ColorPool';
7
import { prepareHeatmapData } from './GraphHeatmapHelpers';
8
import { GraphDisplayMode } from './Panel';
10
export const formatValue = (y: number | null): string => {
14
const absY = Math.abs(y);
17
return (y / 1e24).toFixed(2) + 'Y';
18
} else if (absY >= 1e21) {
19
return (y / 1e21).toFixed(2) + 'Z';
20
} else if (absY >= 1e18) {
21
return (y / 1e18).toFixed(2) + 'E';
22
} else if (absY >= 1e15) {
23
return (y / 1e15).toFixed(2) + 'P';
24
} else if (absY >= 1e12) {
25
return (y / 1e12).toFixed(2) + 'T';
26
} else if (absY >= 1e9) {
27
return (y / 1e9).toFixed(2) + 'G';
28
} else if (absY >= 1e6) {
29
return (y / 1e6).toFixed(2) + 'M';
30
} else if (absY >= 1e3) {
31
return (y / 1e3).toFixed(2) + 'k';
32
} else if (absY >= 1) {
34
} else if (absY === 0) {
36
} else if (absY < 1e-23) {
37
return (y / 1e-24).toFixed(2) + 'y';
38
} else if (absY < 1e-20) {
39
return (y / 1e-21).toFixed(2) + 'z';
40
} else if (absY < 1e-17) {
41
return (y / 1e-18).toFixed(2) + 'a';
42
} else if (absY < 1e-14) {
43
return (y / 1e-15).toFixed(2) + 'f';
44
} else if (absY < 1e-11) {
45
return (y / 1e-12).toFixed(2) + 'p';
46
} else if (absY < 1e-8) {
47
return (y / 1e-9).toFixed(2) + 'n';
48
} else if (absY < 1e-5) {
49
return (y / 1e-6).toFixed(2) + 'µ';
50
} else if (absY < 1e-2) {
51
return (y / 1e-3).toFixed(2) + 'm';
52
} else if (absY <= 1) {
55
throw Error("couldn't format a value, this is a bug");
58
export const getHoverColor = (color: string, opacity: number, stacked: boolean): string => {
59
const { r, g, b } = $.color.parse(color);
61
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
64
Unfortunately flot doesn't take into consideration
65
the alpha value when adjusting the color on the stacked series.
66
TODO: find better way to set the opacity.
68
const base = (1 - opacity) * 255;
69
return `rgb(${Math.round(base + opacity * r)},${Math.round(base + opacity * g)},${Math.round(base + opacity * b)})`;
72
export const toHoverColor =
73
(index: number, stacked: boolean) =>
77
): { color: string; data: (number | null)[][]; index: number; labels: { [p: string]: string } } => ({
79
color: getHoverColor(series.color, i !== index ? 0.3 : 1, stacked),
82
export const getOptions = (stacked: boolean, useLocalTime: boolean): jquery.flot.plotOptions => {
88
mouseActiveRadius: 100,
97
timeBase: 'milliseconds',
98
timezone: useLocalTime ? 'browser' : undefined,
101
tickFormatter: formatValue,
109
cssClass: 'graph-tooltip',
110
content: (_, xval, yval, { series }): string => {
111
const both = series as GraphExemplar | GraphSeries;
112
const { labels, color } = both;
113
let dateTime = moment(xval);
115
dateTime = dateTime.utc();
118
const formatLabels = (labels: { [key: string]: string }): string => `
120
${Object.keys(labels).length === 0 ? '<div class="mb-1 font-italic">no labels</div>' : ''}
121
${labels['__name__'] ? `<div class="mb-1"><strong>${labels['__name__']}</strong></div>` : ''}
122
${Object.keys(labels)
123
.filter((k) => k !== '__name__')
124
.map((k) => `<div class="mb-1"><strong>${k}</strong>: ${escapeHTML(labels[k])}</div>`)
129
<div class="date">${dateTime.format('YYYY-MM-DD HH:mm:ss Z')}</div>
131
<span class="detail-swatch" style="background-color: ${color}"></span>
132
<span>${labels.__name__ || 'value'}: <strong>${yval}</strong></span>
134
<div class="mt-2 mb-1 font-weight-bold">${'seriesLabels' in both ? 'Trace exemplar:' : 'Series:'}</div>
135
${formatLabels(labels)}
137
'seriesLabels' in both
139
<div class="mt-2 mb-1 font-weight-bold">Associated series:</div>${formatLabels(both.seriesLabels)}
149
stack: false, // Stacking is set on a per-series basis because exemplar symbols don't support it.
152
lineWidth: stacked ? 1 : 2,
164
export const normalizeData = ({ queryParams, data, exemplars, displayMode }: GraphProps): GraphData => {
165
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
166
const { startTime, endTime, resolution } = queryParams!;
169
const values: number[] = [];
170
// Exemplars are grouped into buckets by time to use for de-densifying.
171
const buckets: { [time: number]: GraphExemplar[] } = {};
172
for (const exemplar of exemplars || []) {
173
for (const { labels, value, timestamp } of exemplar.exemplars) {
174
const parsed = parseValue(value) || 0;
178
const bucketTime = Math.floor((timestamp / ((endTime - startTime) / 60)) * 0.8) * 1000;
179
if (!buckets[bucketTime]) {
180
buckets[bucketTime] = [];
183
buckets[bucketTime].push({
184
seriesLabels: exemplar.seriesLabels,
186
data: [[timestamp * 1000, parsed]],
187
points: { symbol: exemplarSymbol },
192
const deviation = stdDeviation(sum, values);
194
const series = data.result.map(({ values, histograms, metric }, index) => {
195
// Insert nulls for all missing steps.
198
let histogramPos = 0;
200
for (let t = startTime; t <= endTime; t += resolution) {
201
// Allow for floating point inaccuracy.
202
const currentValue = values && values[valuePos];
203
const currentHistogram = histograms && histograms[histogramPos];
204
if (currentValue && values.length > valuePos && currentValue[0] < t + resolution / 100) {
205
data.push([currentValue[0] * 1000, parseValue(currentValue[1])]);
207
} else if (currentHistogram && histograms.length > histogramPos && currentHistogram[0] < t + resolution / 100) {
208
data.push([currentHistogram[0] * 1000, parseValue(currentHistogram[1].sum)]);
211
data.push([t * 1000, null]);
215
labels: metric !== null ? metric : {},
216
color: colorPool[index % colorPool.length],
217
stack: displayMode === GraphDisplayMode.Stacked,
224
series: displayMode === GraphDisplayMode.Heatmap ? prepareHeatmapData(series) : series,
225
exemplars: Object.values(buckets).flatMap((bucket) => {
226
if (bucket.length === 1) {
230
.sort((a, b) => exValue(b) - exValue(a)) // Sort exemplars by value in descending order.
231
.reduce((exemplars: GraphExemplar[], exemplar) => {
232
if (exemplars.length === 0) {
233
exemplars.push(exemplar);
235
const prev = exemplars[exemplars.length - 1];
236
// Don't plot this exemplar if it's less than two times the standard
237
// deviation spaced from the last.
238
if (exValue(prev) - exValue(exemplar) >= 2 * deviation) {
239
exemplars.push(exemplar);
248
export const parseValue = (value: string): null | number => {
249
const val = parseFloat(value);
250
// "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They
251
// can't be graphed, so show them as gaps (null).
252
return isNaN(val) ? null : val;
255
const exemplarSymbol = (ctx: CanvasRenderingContext2D, x: number, y: number) => {
256
// Center the symbol on the point.
259
// Correct if the symbol is overflowing off the grid.
260
if (x > ctx.canvas.clientWidth - 59) {
261
x = ctx.canvas.clientWidth - 59;
263
if (y > ctx.canvas.clientHeight - 40) {
264
y = ctx.canvas.clientHeight - 40;
268
ctx.rotate(Math.PI / 4);
269
ctx.translate(-x, -y);
271
ctx.fillStyle = '#92bce1';
272
ctx.fillRect(x, y, 7, 7);
274
ctx.strokeStyle = '#0275d8';
276
ctx.strokeRect(x, y, 7, 7);
279
const stdDeviation = (sum: number, values: number[]): number => {
280
const avg = sum / values.length;
282
values.map((value) => (squaredAvg += (value - avg) ** 2));
283
squaredAvg = squaredAvg / values.length;
284
return Math.sqrt(squaredAvg);
287
const exValue = (exemplar: GraphExemplar): number => exemplar.data[0][1];