1import $ from 'jquery';
2
3import { escapeHTML } from '../utils/html';
4import { Metric } from '../types/types';
5import { GraphProps, GraphSeries } from './Graph';
6
7export const formatValue = (y: number | null): string => {
8  if (y === null) {
9    return 'null';
10  }
11  const absY = Math.abs(y);
12
13  if (absY >= 1e24) {
14    return (y / 1e24).toFixed(2) + 'Y';
15  } else if (absY >= 1e21) {
16    return (y / 1e21).toFixed(2) + 'Z';
17  } else if (absY >= 1e18) {
18    return (y / 1e18).toFixed(2) + 'E';
19  } else if (absY >= 1e15) {
20    return (y / 1e15).toFixed(2) + 'P';
21  } else if (absY >= 1e12) {
22    return (y / 1e12).toFixed(2) + 'T';
23  } else if (absY >= 1e9) {
24    return (y / 1e9).toFixed(2) + 'G';
25  } else if (absY >= 1e6) {
26    return (y / 1e6).toFixed(2) + 'M';
27  } else if (absY >= 1e3) {
28    return (y / 1e3).toFixed(2) + 'k';
29  } else if (absY >= 1) {
30    return y.toFixed(2);
31  } else if (absY === 0) {
32    return y.toFixed(2);
33  } else if (absY < 1e-23) {
34    return (y / 1e-24).toFixed(2) + 'y';
35  } else if (absY < 1e-20) {
36    return (y / 1e-21).toFixed(2) + 'z';
37  } else if (absY < 1e-17) {
38    return (y / 1e-18).toFixed(2) + 'a';
39  } else if (absY < 1e-14) {
40    return (y / 1e-15).toFixed(2) + 'f';
41  } else if (absY < 1e-11) {
42    return (y / 1e-12).toFixed(2) + 'p';
43  } else if (absY < 1e-8) {
44    return (y / 1e-9).toFixed(2) + 'n';
45  } else if (absY < 1e-5) {
46    return (y / 1e-6).toFixed(2) + 'µ';
47  } else if (absY < 1e-2) {
48    return (y / 1e-3).toFixed(2) + 'm';
49  } else if (absY <= 1) {
50    return y.toFixed(2);
51  }
52  throw Error("couldn't format a value, this is a bug");
53};
54
55export const getHoverColor = (color: string, opacity: number, stacked: boolean) => {
56  const { r, g, b } = $.color.parse(color);
57  if (!stacked) {
58    return `rgba(${r}, ${g}, ${b}, ${opacity})`;
59  }
60  /*
61    Unfortunately flot doesn't take into consideration
62    the alpha value when adjusting the color on the stacked series.
63    TODO: find better way to set the opacity.
64  */
65  const base = (1 - opacity) * 255;
66  return `rgb(${Math.round(base + opacity * r)},${Math.round(base + opacity * g)},${Math.round(base + opacity * b)})`;
67};
68
69export const toHoverColor = (index: number, stacked: boolean) => (series: GraphSeries, i: number) => ({
70  ...series,
71  color: getHoverColor(series.color, i !== index ? 0.3 : 1, stacked),
72});
73
74export const getOptions = (stacked: boolean): jquery.flot.plotOptions => {
75  return {
76    grid: {
77      hoverable: true,
78      clickable: true,
79      autoHighlight: true,
80      mouseActiveRadius: 100,
81    },
82    legend: {
83      show: false,
84    },
85    xaxis: {
86      mode: 'time',
87      showTicks: true,
88      showMinorTicks: true,
89      timeBase: 'milliseconds',
90    },
91    yaxis: {
92      tickFormatter: formatValue,
93    },
94    crosshair: {
95      mode: 'xy',
96      color: '#bbb',
97    },
98    tooltip: {
99      show: true,
100      cssClass: 'graph-tooltip',
101      content: (_, xval, yval, { series }): string => {
102        const { labels, color } = series;
103        return `
104            <div class="date">${new Date(xval).toUTCString()}</div>
105            <div>
106              <span class="detail-swatch" style="background-color: ${color}" />
107              <span>${labels.__name__ || 'value'}: <strong>${yval}</strong></span>
108            <div>
109            <div class="labels mt-1">
110              ${Object.keys(labels)
111                .map(k =>
112                  k !== '__name__' ? `<div class="mb-1"><strong>${k}</strong>: ${escapeHTML(labels[k])}</div>` : ''
113                )
114                .join('')}
115            </div>
116          `;
117      },
118      defaultTheme: false,
119      lines: true,
120    },
121    series: {
122      stack: stacked,
123      lines: {
124        lineWidth: stacked ? 1 : 2,
125        steps: false,
126        fill: stacked,
127      },
128      shadowSize: 0,
129    },
130  };
131};
132
133// This was adapted from Flot's color generation code.
134export const getColors = (data: { resultType: string; result: Array<{ metric: Metric; values: [number, string][] }> }) => {
135  const colorPool = ['#edc240', '#afd8f8', '#cb4b4b', '#4da74d', '#9440ed'];
136  const colorPoolSize = colorPool.length;
137  let variation = 0;
138  return data.result.map((_, i) => {
139    // Each time we exhaust the colors in the pool we adjust
140    // a scaling factor used to produce more variations on
141    // those colors. The factor alternates negative/positive
142    // to produce lighter/darker colors.
143
144    // Reset the variation after every few cycles, or else
145    // it will end up producing only white or black colors.
146
147    if (i % colorPoolSize === 0 && i) {
148      if (variation >= 0) {
149        variation = variation < 0.5 ? -variation - 0.2 : 0;
150      } else {
151        variation = -variation;
152      }
153    }
154    return $.color.parse(colorPool[i % colorPoolSize] || '#666').scale('rgb', 1 + variation);
155  });
156};
157
158export const normalizeData = ({ stacked, queryParams, data }: GraphProps): GraphSeries[] => {
159  const colors = getColors(data);
160  const { startTime, endTime, resolution } = queryParams!;
161  return data.result.map(({ values, metric }, index) => {
162    // Insert nulls for all missing steps.
163    const data = [];
164    let pos = 0;
165
166    for (let t = startTime; t <= endTime; t += resolution) {
167      // Allow for floating point inaccuracy.
168      const currentValue = values[pos];
169      if (values.length > pos && currentValue[0] < t + resolution / 100) {
170        data.push([currentValue[0] * 1000, parseValue(currentValue[1], stacked)]);
171        pos++;
172      } else {
173        // TODO: Flot has problems displaying intermittent "null" values when stacked,
174        // resort to 0 now. In Grafana this works for some reason, figure out how they
175        // do it.
176        data.push([t * 1000, stacked ? 0 : null]);
177      }
178    }
179
180    return {
181      labels: metric !== null ? metric : {},
182      color: colors[index].toString(),
183      data,
184      index,
185    };
186  });
187};
188
189export const parseValue = (value: string, stacked: boolean) => {
190  const val = parseFloat(value);
191  if (isNaN(val)) {
192    // "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They
193    // can't be graphed, so show them as gaps (null).
194
195    // TODO: Flot has problems displaying intermittent "null" values when stacked,
196    // resort to 0 now. In Grafana this works for some reason, figure out how they
197    // do it.
198    return stacked ? 0 : null;
199  }
200  return val;
201};
202