import $ from 'jquery'; import React, { PureComponent } from 'react'; import ReactResizeDetector from 'react-resize-detector'; import { Alert } from 'reactstrap'; import Legend from './Legend'; import { escapeHTML } from './utils/html'; require('flot'); require('flot/source/jquery.flot.crosshair'); require('flot/source/jquery.flot.legend'); require('flot/source/jquery.flot.time'); require('flot/source/jquery.canvaswrapper'); require('jquery.flot.tooltip'); let graphID = 0; function getGraphID() { // TODO: This is ugly. return graphID++; } interface GraphProps { data: any; // TODO: Type this. stacked: boolean; queryParams: { startTime: number; endTime: number; resolution: number; } | null; } class Graph extends PureComponent { private id: number = getGraphID(); private chartRef = React.createRef(); renderLabels(labels: { [key: string]: string }) { let labelStrings: string[] = []; for (const label in labels) { if (label !== '__name__') { labelStrings.push('' + label + ': ' + escapeHTML(labels[label])); } } return '
' + labelStrings.join('
') + '
'; } formatValue = (y: number | null): string => { if (y === null) { return 'null'; } const abs_y = Math.abs(y); if (abs_y >= 1e24) { return (y / 1e24).toFixed(2) + 'Y'; } else if (abs_y >= 1e21) { return (y / 1e21).toFixed(2) + 'Z'; } else if (abs_y >= 1e18) { return (y / 1e18).toFixed(2) + 'E'; } else if (abs_y >= 1e15) { return (y / 1e15).toFixed(2) + 'P'; } else if (abs_y >= 1e12) { return (y / 1e12).toFixed(2) + 'T'; } else if (abs_y >= 1e9) { return (y / 1e9).toFixed(2) + 'G'; } else if (abs_y >= 1e6) { return (y / 1e6).toFixed(2) + 'M'; } else if (abs_y >= 1e3) { return (y / 1e3).toFixed(2) + 'k'; } else if (abs_y >= 1) { return y.toFixed(2); } else if (abs_y === 0) { return y.toFixed(2); } else if (abs_y <= 1e-24) { return (y / 1e-24).toFixed(2) + 'y'; } else if (abs_y <= 1e-21) { return (y / 1e-21).toFixed(2) + 'z'; } else if (abs_y <= 1e-18) { return (y / 1e-18).toFixed(2) + 'a'; } else if (abs_y <= 1e-15) { return (y / 1e-15).toFixed(2) + 'f'; } else if (abs_y <= 1e-12) { return (y / 1e-12).toFixed(2) + 'p'; } else if (abs_y <= 1e-9) { return (y / 1e-9).toFixed(2) + 'n'; } else if (abs_y <= 1e-6) { return (y / 1e-6).toFixed(2) + 'ยต'; } else if (abs_y <= 1e-3) { return (y / 1e-3).toFixed(2) + 'm'; } else if (abs_y <= 1) { return y.toFixed(2); } throw Error("couldn't format a value, this is a bug"); }; getOptions(): any { return { grid: { hoverable: true, clickable: true, autoHighlight: true, mouseActiveRadius: 100, }, legend: { show: false, }, xaxis: { mode: 'time', showTicks: true, showMinorTicks: true, timeBase: 'milliseconds', }, yaxis: { tickFormatter: this.formatValue, }, crosshair: { mode: 'xy', color: '#bbb', }, tooltip: { show: true, cssClass: 'graph-tooltip', content: (label: string, xval: number, yval: number, flotItem: any) => { const series = flotItem.series; // TODO: type this. const date = '' + new Date(xval).toUTCString() + ''; const swatch = ''; const content = swatch + (series.labels.__name__ || 'value') + ': ' + yval + ''; return date + '
' + content + '
' + this.renderLabels(series.labels); }, defaultTheme: false, lines: true, }, series: { stack: this.props.stacked, lines: { lineWidth: this.props.stacked ? 1 : 2, steps: false, fill: this.props.stacked, }, shadowSize: 0, }, }; } // This was adapted from Flot's color generation code. getColors() { const colors = []; const colorPool = ['#edc240', '#afd8f8', '#cb4b4b', '#4da74d', '#9440ed']; const colorPoolSize = colorPool.length; let variation = 0; const neededColors = this.props.data.result.length; for (let i = 0; i < neededColors; i++) { const c = ($ as any).color.parse(colorPool[i % colorPoolSize] || '#666'); // Each time we exhaust the colors in the pool we adjust // a scaling factor used to produce more variations on // those colors. The factor alternates negative/positive // to produce lighter/darker colors. // Reset the variation after every few cycles, or else // it will end up producing only white or black colors. if (i % colorPoolSize === 0 && i) { if (variation >= 0) { if (variation < 0.5) { variation = -variation - 0.2; } else variation = 0; } else variation = -variation; } colors[i] = c.scale('rgb', 1 + variation); } return colors; } getData() { const colors = this.getColors(); return this.props.data.result.map((ts: any /* TODO: Type this*/, index: number) => { // Insert nulls for all missing steps. const data = []; let pos = 0; const params = this.props.queryParams!; for (let t = params.startTime; t <= params.endTime; t += params.resolution) { // Allow for floating point inaccuracy. if (ts.values.length > pos && ts.values[pos][0] < t + params.resolution / 100) { data.push([ts.values[pos][0] * 1000, this.parseValue(ts.values[pos][1])]); pos++; } else { // TODO: Flot has problems displaying intermittent "null" values when stacked, // resort to 0 now. In Grafana this works for some reason, figure out how they // do it. data.push([t * 1000, this.props.stacked ? 0 : null]); } } return { labels: ts.metric !== null ? ts.metric : {}, data: data, color: colors[index], index: index, }; }); } parseValue(value: string) { const val = parseFloat(value); if (isNaN(val)) { // "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They // can't be graphed, so show them as gaps (null). // TODO: Flot has problems displaying intermittent "null" values when stacked, // resort to 0 now. In Grafana this works for some reason, figure out how they // do it. return this.props.stacked ? 0 : null; } return val; } componentDidMount() { this.plot(); } componentDidUpdate() { this.plot(); } componentWillUnmount() { this.destroyPlot(); } plot() { if (this.chartRef.current === null) { return; } this.destroyPlot(); $.plot($(this.chartRef.current!), this.getData(), this.getOptions()); } destroyPlot() { const chart = $(this.chartRef.current!).data('plot'); if (chart !== undefined) { chart.destroy(); } } render() { if (this.props.data === null) { return No data queried yet; } if (this.props.data.resultType !== 'matrix') { return ( Query result is of wrong type '{this.props.data.resultType}', should be 'matrix' (range vector). ); } if (this.props.data.result.length === 0) { return Empty query result; } return (
this.plot()} />
); } } export default Graph;