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