github.com/thanos-io/thanos@v0.32.5/pkg/ui/react-app/src/pages/graph/GraphHelpers.ts (about) 1 import $ from 'jquery'; 2 3 import { escapeHTML } from '../../utils'; 4 import { GraphProps, GraphSeries } from './Graph'; 5 import moment from 'moment-timezone'; 6 import { colorPool } from './ColorPool'; 7 8 export const formatValue = (y: number | null): string => { 9 if (y === null) { 10 return 'null'; 11 } 12 const absY = Math.abs(y); 13 14 if (absY >= 1e24) { 15 return (y / 1e24).toFixed(2) + 'Y'; 16 } else if (absY >= 1e21) { 17 return (y / 1e21).toFixed(2) + 'Z'; 18 } else if (absY >= 1e18) { 19 return (y / 1e18).toFixed(2) + 'E'; 20 } else if (absY >= 1e15) { 21 return (y / 1e15).toFixed(2) + 'P'; 22 } else if (absY >= 1e12) { 23 return (y / 1e12).toFixed(2) + 'T'; 24 } else if (absY >= 1e9) { 25 return (y / 1e9).toFixed(2) + 'G'; 26 } else if (absY >= 1e6) { 27 return (y / 1e6).toFixed(2) + 'M'; 28 } else if (absY >= 1e3) { 29 return (y / 1e3).toFixed(2) + 'k'; 30 } else if (absY >= 1) { 31 return y.toFixed(2); 32 } else if (absY === 0) { 33 return y.toFixed(2); 34 } else if (absY < 1e-23) { 35 return (y / 1e-24).toFixed(2) + 'y'; 36 } else if (absY < 1e-20) { 37 return (y / 1e-21).toFixed(2) + 'z'; 38 } else if (absY < 1e-17) { 39 return (y / 1e-18).toFixed(2) + 'a'; 40 } else if (absY < 1e-14) { 41 return (y / 1e-15).toFixed(2) + 'f'; 42 } else if (absY < 1e-11) { 43 return (y / 1e-12).toFixed(2) + 'p'; 44 } else if (absY < 1e-8) { 45 return (y / 1e-9).toFixed(2) + 'n'; 46 } else if (absY < 1e-5) { 47 return (y / 1e-6).toFixed(2) + 'ยต'; 48 } else if (absY < 1e-2) { 49 return (y / 1e-3).toFixed(2) + 'm'; 50 } else if (absY <= 1) { 51 return y.toFixed(2); 52 } 53 throw Error("couldn't format a value, this is a bug"); 54 }; 55 56 export const getHoverColor = (color: string, opacity: number, stacked: boolean): string => { 57 const { r, g, b } = $.color.parse(color); 58 if (!stacked) { 59 return `rgba(${r}, ${g}, ${b}, ${opacity})`; 60 } 61 /* 62 Unfortunately flot doesn't take into consideration 63 the alpha value when adjusting the color on the stacked series. 64 TODO: find better way to set the opacity. 65 */ 66 const base = (1 - opacity) * 255; 67 return `rgb(${Math.round(base + opacity * r)},${Math.round(base + opacity * g)},${Math.round(base + opacity * b)})`; 68 }; 69 70 export const toHoverColor = (index: number, stacked: boolean) => (series: GraphSeries, i: number) => ({ 71 ...series, 72 color: getHoverColor(series.color, i !== index ? 0.3 : 1, stacked), 73 }); 74 75 export const getOptions = (stacked: boolean, useLocalTime: boolean): jquery.flot.plotOptions => { 76 return { 77 grid: { 78 hoverable: true, 79 clickable: true, 80 autoHighlight: true, 81 mouseActiveRadius: 100, 82 }, 83 legend: { 84 show: false, 85 }, 86 xaxis: { 87 mode: 'time', 88 showTicks: true, 89 showMinorTicks: true, 90 timeBase: 'milliseconds', 91 timezone: useLocalTime ? 'browser' : undefined, 92 }, 93 yaxis: { 94 tickFormatter: formatValue, 95 }, 96 crosshair: { 97 mode: 'xy', 98 color: '#bbb', 99 }, 100 tooltip: { 101 show: true, 102 cssClass: 'graph-tooltip', 103 content: (_, xval, yval, { series }): string => { 104 const { labels, color } = series; 105 let dateTime = moment(xval); 106 if (!useLocalTime) { 107 dateTime = dateTime.utc(); 108 } 109 return ` 110 <div class="date">${dateTime.format('YYYY-MM-DD HH:mm:ss Z')}</div> 111 <div> 112 <span class="detail-swatch" style="background-color: ${color}"></span> 113 <span>${labels.__name__ || 'value'}: <strong>${yval}</strong></span> 114 <div> 115 <div class="labels mt-1"> 116 ${Object.keys(labels) 117 .map((k) => 118 k !== '__name__' ? `<div class="mb-1"><strong>${k}</strong>: ${escapeHTML(labels[k])}</div>` : '' 119 ) 120 .join('')} 121 </div> 122 `; 123 }, 124 defaultTheme: false, 125 lines: true, 126 }, 127 series: { 128 stack: stacked, 129 lines: { 130 lineWidth: stacked ? 1 : 2, 131 steps: false, 132 fill: stacked, 133 }, 134 shadowSize: 0, 135 }, 136 }; 137 }; 138 139 export const normalizeData = ({ queryParams, data }: GraphProps): GraphSeries[] => { 140 const { startTime, endTime, resolution } = queryParams!; 141 return data.result.map(({ values, histograms, metric }, index) => { 142 // Insert nulls for all missing steps. 143 const data = []; 144 let valPos = 0; 145 let histogramPos = 0; 146 147 for (let t = startTime; t <= endTime; t += resolution) { 148 // Allow for floating point inaccuracy. 149 const currentValue = values && values[valPos]; 150 const currentHistogram = histograms && histograms[histogramPos]; 151 if (currentValue && values.length > valPos && currentValue[0] < t + resolution / 100) { 152 data.push([currentValue[0] * 1000, parseValue(currentValue[1])]); 153 valPos++; 154 } else if (currentHistogram && histograms.length > histogramPos && currentHistogram[0] < t + resolution / 100) { 155 data.push([currentHistogram[0] * 1000, parseValue(currentHistogram[1].sum)]); 156 histogramPos++; 157 } else { 158 data.push([t * 1000, null]); 159 } 160 } 161 162 return { 163 labels: metric !== null ? metric : {}, 164 color: colorPool[index % colorPool.length], 165 data, 166 index, 167 }; 168 }); 169 }; 170 171 export const parseValue = (value: string): null | number => { 172 const val = parseFloat(value); 173 // "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They 174 // can't be graphed, so show them as gaps (null). 175 return isNaN(val) ? null : val; 176 };