mirror of https://github.com/prometheus/prometheus
ReactUI: adopt grafana flot fix for stacked graphs (#6603)
* adopt grafana flot fix for stacked graphs Signed-off-by: blalov <boiskila@gmail.com> Signed-off-by: Boyko Lalov <boiskila@gmail.com> * remove flot as dependencie Signed-off-by: blalov <boiskila@gmail.com> Signed-off-by: Boyko Lalov <boiskila@gmail.com> * disable eslint error in flot.js Signed-off-by: blalov <boiskila@gmail.com> Signed-off-by: Boyko Lalov <boiskila@gmail.com> * fix broken test due to wrong import Signed-off-by: blalov <boiskila@gmail.com> Signed-off-by: Boyko Lalov <boiskila@gmail.com> * bring back flot crosshair plugin Signed-off-by: blalov <boiskila@gmail.com> Signed-off-by: Boyko Lalov <boiskila@gmail.com> * trying to prevent CI out of memory by adding test script --runInBand flag Signed-off-by: blalov <boiskila@gmail.com> Signed-off-by: Boyko Lalov <boiskila@gmail.com> * additional notices regarding origin of the flot vendor Signed-off-by: Boyko Lalov <boiskila@gmail.com> * move flot in own folder Signed-off-by: Boyko Lalov <boiskila@gmail.com> * move text on top Signed-off-by: Boyko Lalov <boiskila@gmail.com>pull/6557/head
parent
3b9304d22e
commit
e12e5ecc8f
|
@ -20,7 +20,6 @@
|
|||
"bootstrap": "^4.2.1",
|
||||
"downshift": "^3.2.2",
|
||||
"enzyme-to-json": "^3.4.3",
|
||||
"flot": "^3.2.13",
|
||||
"fuzzy": "^0.1.3",
|
||||
"i": "^0.3.6",
|
||||
"jest-fetch-mock": "^2.1.2",
|
||||
|
@ -45,7 +44,7 @@
|
|||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"test": "react-scripts test --runInBand",
|
||||
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
|
||||
"eject": "react-scripts eject",
|
||||
"lint:ci": "eslint --quiet \"src/**/*.{ts,tsx}\"",
|
||||
|
|
|
@ -104,7 +104,7 @@ describe('Graph', () => {
|
|||
chartData: [
|
||||
{
|
||||
color: 'rgb(237,194,64)',
|
||||
data: [[1572128592000, 0]],
|
||||
data: [[1572128592000, null]],
|
||||
index: 0,
|
||||
labels: {},
|
||||
},
|
||||
|
|
|
@ -7,11 +7,10 @@ import { Metric, QueryParams } from '../types/types';
|
|||
import { isPresent } from '../utils/func';
|
||||
import { normalizeData, getOptions, toHoverColor } from './GraphHelpers';
|
||||
|
||||
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('../vendor/flot/jquery.flot');
|
||||
require('../vendor/flot/jquery.flot.stack');
|
||||
require('../vendor/flot/jquery.flot.time');
|
||||
require('../vendor/flot/jquery.flot.crosshair');
|
||||
require('jquery.flot.tooltip');
|
||||
|
||||
export interface GraphProps {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { formatValue, getColors, parseValue, getOptions } from './GraphHelpers';
|
||||
require('flot'); // need for $.colors
|
||||
require('../vendor/flot/jquery.flot'); // need for $.colors
|
||||
|
||||
describe('GraphHelpers', () => {
|
||||
describe('formatValue', () => {
|
||||
|
@ -88,13 +88,13 @@ describe('GraphHelpers', () => {
|
|||
});
|
||||
describe('parseValue', () => {
|
||||
it('should parse number properly', () => {
|
||||
expect(parseValue('12.3e', true)).toEqual(12.3);
|
||||
expect(parseValue('12.3e')).toEqual(12.3);
|
||||
});
|
||||
it('should return 0 if value is NaN and stacked prop is true', () => {
|
||||
expect(parseValue('asd', true)).toEqual(0);
|
||||
expect(parseValue('asd')).toEqual(null);
|
||||
});
|
||||
it('should return null if value is NaN and stacked prop is false', () => {
|
||||
expect(parseValue('asd', false)).toBeNull();
|
||||
expect(parseValue('asd')).toBeNull();
|
||||
});
|
||||
});
|
||||
describe('Plot options', () => {
|
||||
|
|
|
@ -155,7 +155,7 @@ export const getColors = (data: { resultType: string; result: Array<{ metric: Me
|
|||
});
|
||||
};
|
||||
|
||||
export const normalizeData = ({ stacked, queryParams, data }: GraphProps): GraphSeries[] => {
|
||||
export const normalizeData = ({ queryParams, data }: GraphProps): GraphSeries[] => {
|
||||
const colors = getColors(data);
|
||||
const { startTime, endTime, resolution } = queryParams!;
|
||||
return data.result.map(({ values, metric }, index) => {
|
||||
|
@ -167,13 +167,10 @@ export const normalizeData = ({ stacked, queryParams, data }: GraphProps): Graph
|
|||
// Allow for floating point inaccuracy.
|
||||
const currentValue = values[pos];
|
||||
if (values.length > pos && currentValue[0] < t + resolution / 100) {
|
||||
data.push([currentValue[0] * 1000, parseValue(currentValue[1], stacked)]);
|
||||
data.push([currentValue[0] * 1000, parseValue(currentValue[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, stacked ? 0 : null]);
|
||||
data.push([t * 1000, null]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,16 +183,9 @@ export const normalizeData = ({ stacked, queryParams, data }: GraphProps): Graph
|
|||
});
|
||||
};
|
||||
|
||||
export const parseValue = (value: string, stacked: boolean) => {
|
||||
export const 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 stacked ? 0 : null;
|
||||
}
|
||||
return val;
|
||||
// "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They
|
||||
// can't be graphed, so show them as gaps (null).
|
||||
return isNaN(val) ? null : val;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
/**
|
||||
*
|
||||
* THIS FILE WAS COPIED INTO PROMETHEUS FROM GRAFANA'S VENDORED FORK OF FLOT
|
||||
* (LIVING AT https://github.com/grafana/grafana/tree/master/public/vendor/flot),
|
||||
* WHICH CONTAINS FIXES FOR DISPLAYING NULL VALUES IN STACKED GRAPHS. THE ORIGINAL
|
||||
* FLOT CODE WAS LICENSED UNDER THE MIT LICENSE AS STATED BELOW. ADDITIONAL
|
||||
* CHANGES HAVE BEEN CONTRIBUTED TO THE GRAFANA FORK UNDER AN APACHE 2 LICENSE, SEE
|
||||
* https://github.com/grafana/grafana/blob/master/license.
|
||||
*
|
||||
*/
|
||||
|
||||
/* eslint-disable prefer-spread */
|
||||
/* eslint-disable no-loop-func */
|
||||
/* eslint-disable @typescript-eslint/no-this-alias */
|
||||
/* eslint-disable no-redeclare */
|
||||
/* eslint-disable no-useless-escape */
|
||||
/* eslint-disable prefer-const */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
/* eslint-disable eqeqeq */
|
||||
/* eslint-disable no-var */
|
||||
/* Flot plugin for showing crosshairs when the mouse hovers over the plot.
|
||||
|
||||
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||
Licensed under the MIT license.
|
||||
|
||||
The plugin supports these options:
|
||||
|
||||
crosshair: {
|
||||
mode: null or "x" or "y" or "xy"
|
||||
color: color
|
||||
lineWidth: number
|
||||
}
|
||||
|
||||
Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical
|
||||
crosshair that lets you trace the values on the x axis, "y" enables a
|
||||
horizontal crosshair and "xy" enables them both. "color" is the color of the
|
||||
crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of
|
||||
the drawn lines (default is 1).
|
||||
|
||||
The plugin also adds four public methods:
|
||||
|
||||
- setCrosshair( pos )
|
||||
|
||||
Set the position of the crosshair. Note that this is cleared if the user
|
||||
moves the mouse. "pos" is in coordinates of the plot and should be on the
|
||||
form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple
|
||||
axes), which is coincidentally the same format as what you get from a
|
||||
"plothover" event. If "pos" is null, the crosshair is cleared.
|
||||
|
||||
- clearCrosshair()
|
||||
|
||||
Clear the crosshair.
|
||||
|
||||
- lockCrosshair(pos)
|
||||
|
||||
Cause the crosshair to lock to the current location, no longer updating if
|
||||
the user moves the mouse. Optionally supply a position (passed on to
|
||||
setCrosshair()) to move it to.
|
||||
|
||||
Example usage:
|
||||
|
||||
var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } };
|
||||
$("#graph").bind( "plothover", function ( evt, position, item ) {
|
||||
if ( item ) {
|
||||
// Lock the crosshair to the data point being hovered
|
||||
myFlot.lockCrosshair({
|
||||
x: item.datapoint[ 0 ],
|
||||
y: item.datapoint[ 1 ]
|
||||
});
|
||||
} else {
|
||||
// Return normal crosshair operation
|
||||
myFlot.unlockCrosshair();
|
||||
}
|
||||
});
|
||||
|
||||
- unlockCrosshair()
|
||||
|
||||
Free the crosshair to move again after locking it.
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
const options = {
|
||||
crosshair: {
|
||||
mode: null, // one of null, "x", "y" or "xy",
|
||||
color: 'rgba(170, 0, 0, 0.80)',
|
||||
lineWidth: 1,
|
||||
},
|
||||
};
|
||||
|
||||
function init(plot) {
|
||||
// position of crosshair in pixels
|
||||
const crosshair = { x: -1, y: -1, locked: false };
|
||||
|
||||
plot.setCrosshair = function setCrosshair(pos) {
|
||||
if (!pos) crosshair.x = -1;
|
||||
else {
|
||||
const o = plot.p2c(pos);
|
||||
crosshair.x = Math.max(0, Math.min(o.left, plot.width()));
|
||||
crosshair.y = Math.max(0, Math.min(o.top, plot.height()));
|
||||
}
|
||||
|
||||
plot.triggerRedrawOverlay();
|
||||
};
|
||||
|
||||
plot.clearCrosshair = plot.setCrosshair; // passes null for pos
|
||||
|
||||
plot.lockCrosshair = function lockCrosshair(pos) {
|
||||
if (pos) plot.setCrosshair(pos);
|
||||
crosshair.locked = true;
|
||||
};
|
||||
|
||||
plot.unlockCrosshair = function unlockCrosshair() {
|
||||
crosshair.locked = false;
|
||||
};
|
||||
|
||||
function onMouseOut() {
|
||||
if (crosshair.locked) return;
|
||||
|
||||
if (crosshair.x != -1) {
|
||||
crosshair.x = -1;
|
||||
plot.triggerRedrawOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseMove(e) {
|
||||
if (crosshair.locked) return;
|
||||
|
||||
if (plot.getSelection && plot.getSelection()) {
|
||||
crosshair.x = -1; // hide the crosshair while selecting
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = plot.offset();
|
||||
crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
|
||||
crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
|
||||
plot.triggerRedrawOverlay();
|
||||
}
|
||||
|
||||
plot.hooks.bindEvents.push(function(plot, eventHolder) {
|
||||
if (!plot.getOptions().crosshair.mode) return;
|
||||
|
||||
eventHolder.mouseout(onMouseOut);
|
||||
eventHolder.mousemove(onMouseMove);
|
||||
});
|
||||
|
||||
plot.hooks.drawOverlay.push(function(plot, ctx) {
|
||||
const c = plot.getOptions().crosshair;
|
||||
if (!c.mode) return;
|
||||
|
||||
const plotOffset = plot.getPlotOffset();
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(plotOffset.left, plotOffset.top);
|
||||
|
||||
if (crosshair.x != -1) {
|
||||
const adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0;
|
||||
|
||||
ctx.strokeStyle = c.color;
|
||||
ctx.lineWidth = c.lineWidth;
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
ctx.beginPath();
|
||||
if (c.mode.indexOf('x') != -1) {
|
||||
const drawX = Math.floor(crosshair.x) + adj;
|
||||
ctx.moveTo(drawX, 0);
|
||||
ctx.lineTo(drawX, plot.height());
|
||||
}
|
||||
if (c.mode.indexOf('y') != -1) {
|
||||
const drawY = Math.floor(crosshair.y) + adj;
|
||||
ctx.moveTo(0, drawY);
|
||||
ctx.lineTo(plot.width(), drawY);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
plot.hooks.shutdown.push(function(plot, eventHolder) {
|
||||
eventHolder.unbind('mouseout', onMouseOut);
|
||||
eventHolder.unbind('mousemove', onMouseMove);
|
||||
});
|
||||
}
|
||||
|
||||
$.plot.plugins.push({
|
||||
init: init,
|
||||
options: options,
|
||||
name: 'crosshair',
|
||||
version: '1.0',
|
||||
});
|
||||
})(window.jQuery);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,205 @@
|
|||
/**
|
||||
*
|
||||
* THIS FILE WAS COPIED INTO PROMETHEUS FROM GRAFANA'S VENDORED FORK OF FLOT
|
||||
* (LIVING AT https://github.com/grafana/grafana/tree/master/public/vendor/flot),
|
||||
* WHICH CONTAINS FIXES FOR DISPLAYING NULL VALUES IN STACKED GRAPHS. THE ORIGINAL
|
||||
* FLOT CODE WAS LICENSED UNDER THE MIT LICENSE AS STATED BELOW. ADDITIONAL
|
||||
* CHANGES HAVE BEEN CONTRIBUTED TO THE GRAFANA FORK UNDER AN APACHE 2 LICENSE, SEE
|
||||
* https://github.com/grafana/grafana/blob/master/license.
|
||||
*
|
||||
*/
|
||||
|
||||
/* eslint-disable prefer-spread */
|
||||
/* eslint-disable no-loop-func */
|
||||
/* eslint-disable @typescript-eslint/no-this-alias */
|
||||
/* eslint-disable no-redeclare */
|
||||
/* eslint-disable no-useless-escape */
|
||||
/* eslint-disable prefer-const */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
/* eslint-disable eqeqeq */
|
||||
/* eslint-disable no-var */
|
||||
|
||||
/* Flot plugin for stacking data sets rather than overlyaing them.
|
||||
|
||||
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||
Licensed under the MIT license.
|
||||
|
||||
The plugin assumes the data is sorted on x (or y if stacking horizontally).
|
||||
For line charts, it is assumed that if a line has an undefined gap (from a
|
||||
null point), then the line above it should have the same gap - insert zeros
|
||||
instead of "null" if you want another behaviour. This also holds for the start
|
||||
and end of the chart. Note that stacking a mix of positive and negative values
|
||||
in most instances doesn't make sense (so it looks weird).
|
||||
|
||||
Two or more series are stacked when their "stack" attribute is set to the same
|
||||
key (which can be any number or string or just "true"). To specify the default
|
||||
stack, you can set the stack option like this:
|
||||
|
||||
series: {
|
||||
stack: null/false, true, or a key (number/string)
|
||||
}
|
||||
|
||||
You can also specify it for a single series, like this:
|
||||
|
||||
$.plot( $("#placeholder"), [{
|
||||
data: [ ... ],
|
||||
stack: true
|
||||
}])
|
||||
|
||||
The stacking order is determined by the order of the data series in the array
|
||||
(later series end up on top of the previous).
|
||||
|
||||
Internally, the plugin modifies the datapoints in each series, adding an
|
||||
offset to the y value. For line series, extra data points are inserted through
|
||||
interpolation. If there's a second y value, it's also adjusted (e.g for bar
|
||||
charts or filled areas).
|
||||
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
const options = {
|
||||
series: { stack: null }, // or number/string
|
||||
};
|
||||
|
||||
function init(plot) {
|
||||
function findMatchingSeries(s, allseries) {
|
||||
let res = null;
|
||||
for (let i = 0; i < allseries.length; ++i) {
|
||||
if (s == allseries[i]) break;
|
||||
|
||||
if (allseries[i].stack == s.stack) res = allseries[i];
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function stackData(plot, s, datapoints) {
|
||||
if (s.stack == null || s.stack === false) return;
|
||||
|
||||
const other = findMatchingSeries(s, plot.getData());
|
||||
if (!other) return;
|
||||
|
||||
let ps = datapoints.pointsize,
|
||||
points = datapoints.points,
|
||||
otherps = other.datapoints.pointsize,
|
||||
otherpoints = other.datapoints.points,
|
||||
newpoints = [],
|
||||
px,
|
||||
py,
|
||||
intery,
|
||||
qx,
|
||||
qy,
|
||||
bottom,
|
||||
withlines = s.lines.show,
|
||||
horizontal = s.bars.horizontal,
|
||||
withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y),
|
||||
withsteps = withlines && s.lines.steps,
|
||||
keyOffset = horizontal ? 1 : 0,
|
||||
accumulateOffset = horizontal ? 0 : 1,
|
||||
i = 0,
|
||||
j = 0,
|
||||
l,
|
||||
m;
|
||||
|
||||
while (true) {
|
||||
if (i >= points.length && j >= otherpoints.length) break;
|
||||
|
||||
l = newpoints.length;
|
||||
|
||||
if (i < points.length && points[i] == null) {
|
||||
// copy gaps
|
||||
for (m = 0; m < ps; ++m) newpoints.push(points[i + m]);
|
||||
i += ps;
|
||||
} else if (i >= points.length) {
|
||||
// take the remaining points from the previous series
|
||||
for (m = 0; m < ps; ++m) newpoints.push(otherpoints[j + m]);
|
||||
if (withbottom) newpoints[l + 2] = otherpoints[j + accumulateOffset];
|
||||
j += otherps;
|
||||
} else if (j >= otherpoints.length) {
|
||||
// take the remaining points from the current series
|
||||
for (m = 0; m < ps; ++m) newpoints.push(points[i + m]);
|
||||
i += ps;
|
||||
} else if (j < otherpoints.length && otherpoints[j] == null) {
|
||||
// ignore point
|
||||
j += otherps;
|
||||
} else {
|
||||
// cases where we actually got two points
|
||||
px = points[i + keyOffset];
|
||||
py = points[i + accumulateOffset];
|
||||
qx = otherpoints[j + keyOffset];
|
||||
qy = otherpoints[j + accumulateOffset];
|
||||
bottom = 0;
|
||||
|
||||
if (px == qx) {
|
||||
for (m = 0; m < ps; ++m) newpoints.push(points[i + m]);
|
||||
|
||||
newpoints[l + accumulateOffset] += qy;
|
||||
bottom = qy;
|
||||
|
||||
i += ps;
|
||||
j += otherps;
|
||||
} else if (px > qx) {
|
||||
// take the point from the previous series so that next series will correctly stack
|
||||
if (i == 0) {
|
||||
for (m = 0; m < ps; ++m) newpoints.push(otherpoints[j + m]);
|
||||
bottom = qy;
|
||||
}
|
||||
// we got past point below, might need to
|
||||
// insert interpolated extra point
|
||||
if (i > 0 && points[i - ps] != null) {
|
||||
intery = py + ((points[i - ps + accumulateOffset] - py) * (qx - px)) / (points[i - ps + keyOffset] - px);
|
||||
newpoints.push(qx);
|
||||
newpoints.push(intery + qy);
|
||||
for (m = 2; m < ps; ++m) newpoints.push(points[i + m]);
|
||||
bottom = qy;
|
||||
}
|
||||
|
||||
j += otherps;
|
||||
} else {
|
||||
// px < qx
|
||||
for (m = 0; m < ps; ++m) newpoints.push(points[i + m]);
|
||||
|
||||
// we might be able to interpolate a point below,
|
||||
// this can give us a better y
|
||||
if (j > 0 && otherpoints[j - otherps] != null)
|
||||
bottom =
|
||||
qy +
|
||||
((otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx)) /
|
||||
(otherpoints[j - otherps + keyOffset] - qx);
|
||||
|
||||
newpoints[l + accumulateOffset] += bottom;
|
||||
|
||||
i += ps;
|
||||
}
|
||||
|
||||
if (l != newpoints.length && withbottom) newpoints[l + 2] = bottom;
|
||||
}
|
||||
|
||||
// maintain the line steps invariant
|
||||
if (
|
||||
withsteps &&
|
||||
l != newpoints.length &&
|
||||
l > 0 &&
|
||||
newpoints[l] != null &&
|
||||
newpoints[l] != newpoints[l - ps] &&
|
||||
newpoints[l + 1] != newpoints[l - ps + 1]
|
||||
) {
|
||||
for (m = 0; m < ps; ++m) newpoints[l + ps + m] = newpoints[l + m];
|
||||
newpoints[l + 1] = newpoints[l - ps + 1];
|
||||
}
|
||||
}
|
||||
|
||||
datapoints.points = newpoints;
|
||||
}
|
||||
|
||||
plot.hooks.processDatapoints.push(stackData);
|
||||
}
|
||||
|
||||
$.plot.plugins.push({
|
||||
init: init,
|
||||
options: options,
|
||||
name: 'stack',
|
||||
version: '1.2',
|
||||
});
|
||||
})(window.jQuery);
|
|
@ -0,0 +1,482 @@
|
|||
/**
|
||||
*
|
||||
* THIS FILE WAS COPIED INTO PROMETHEUS FROM GRAFANA'S VENDORED FORK OF FLOT
|
||||
* (LIVING AT https://github.com/grafana/grafana/tree/master/public/vendor/flot),
|
||||
* WHICH CONTAINS FIXES FOR DISPLAYING NULL VALUES IN STACKED GRAPHS. THE ORIGINAL
|
||||
* FLOT CODE WAS LICENSED UNDER THE MIT LICENSE AS STATED BELOW. ADDITIONAL
|
||||
* CHANGES HAVE BEEN CONTRIBUTED TO THE GRAFANA FORK UNDER AN APACHE 2 LICENSE, SEE
|
||||
* https://github.com/grafana/grafana/blob/master/license.
|
||||
*
|
||||
*/
|
||||
|
||||
/* eslint-disable prefer-rest-params */
|
||||
/* eslint-disable no-useless-concat */
|
||||
/* eslint-disable default-case */
|
||||
/* eslint-disable prefer-spread */
|
||||
/* eslint-disable no-loop-func */
|
||||
/* eslint-disable @typescript-eslint/no-this-alias */
|
||||
/* eslint-disable no-redeclare */
|
||||
/* eslint-disable no-useless-escape */
|
||||
/* eslint-disable prefer-const */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
/* eslint-disable eqeqeq */
|
||||
/* eslint-disable no-var */
|
||||
|
||||
/* Pretty handling of time axes.
|
||||
|
||||
Copyright (c) 2007-2013 IOLA and Ole Laursen.
|
||||
Licensed under the MIT license.
|
||||
|
||||
Set axis.mode to "time" to enable. See the section "Time series data" in
|
||||
API.txt for details.
|
||||
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
const options = {
|
||||
xaxis: {
|
||||
timezone: null, // "browser" for local to the client or timezone for timezone-js
|
||||
timeformat: null, // format string to use
|
||||
twelveHourClock: false, // 12 or 24 time in time mode
|
||||
monthNames: null, // list of names of months
|
||||
},
|
||||
};
|
||||
|
||||
// round to nearby lower multiple of base
|
||||
|
||||
function floorInBase(n, base) {
|
||||
return base * Math.floor(n / base);
|
||||
}
|
||||
|
||||
// Returns a string with the date d formatted according to fmt.
|
||||
// A subset of the Open Group's strftime format is supported.
|
||||
|
||||
function formatDate(d, fmt, monthNames, dayNames) {
|
||||
if (typeof d.strftime == 'function') {
|
||||
return d.strftime(fmt);
|
||||
}
|
||||
|
||||
const leftPad = function(n, pad) {
|
||||
n = '' + n;
|
||||
pad = '' + (pad == null ? '0' : pad);
|
||||
return n.length == 1 ? pad + n : n;
|
||||
};
|
||||
|
||||
const r = [];
|
||||
let escape = false;
|
||||
const hours = d.getHours();
|
||||
const isAM = hours < 12;
|
||||
|
||||
if (monthNames == null) {
|
||||
monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
}
|
||||
|
||||
if (dayNames == null) {
|
||||
dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
}
|
||||
|
||||
let hours12;
|
||||
|
||||
if (hours > 12) {
|
||||
hours12 = hours - 12;
|
||||
} else if (hours == 0) {
|
||||
hours12 = 12;
|
||||
} else {
|
||||
hours12 = hours;
|
||||
}
|
||||
|
||||
for (let i = 0; i < fmt.length; ++i) {
|
||||
let c = fmt.charAt(i);
|
||||
|
||||
if (escape) {
|
||||
switch (c) {
|
||||
case 'a':
|
||||
c = '' + dayNames[d.getDay()];
|
||||
break;
|
||||
case 'b':
|
||||
c = '' + monthNames[d.getMonth()];
|
||||
break;
|
||||
case 'd':
|
||||
c = leftPad(d.getDate(), '');
|
||||
break;
|
||||
case 'e':
|
||||
c = leftPad(d.getDate(), ' ');
|
||||
break;
|
||||
case 'h': // For back-compat with 0.7; remove in 1.0
|
||||
case 'H':
|
||||
c = leftPad(hours);
|
||||
break;
|
||||
case 'I':
|
||||
c = leftPad(hours12);
|
||||
break;
|
||||
case 'l':
|
||||
c = leftPad(hours12, ' ');
|
||||
break;
|
||||
case 'm':
|
||||
c = leftPad(d.getMonth() + 1, '');
|
||||
break;
|
||||
case 'M':
|
||||
c = leftPad(d.getMinutes());
|
||||
break;
|
||||
// quarters not in Open Group's strftime specification
|
||||
case 'q':
|
||||
c = '' + (Math.floor(d.getMonth() / 3) + 1);
|
||||
break;
|
||||
case 'S':
|
||||
c = leftPad(d.getSeconds());
|
||||
break;
|
||||
case 'y':
|
||||
c = leftPad(d.getFullYear() % 100);
|
||||
break;
|
||||
case 'Y':
|
||||
c = '' + d.getFullYear();
|
||||
break;
|
||||
case 'p':
|
||||
c = isAM ? '' + 'am' : '' + 'pm';
|
||||
break;
|
||||
case 'P':
|
||||
c = isAM ? '' + 'AM' : '' + 'PM';
|
||||
break;
|
||||
case 'w':
|
||||
c = '' + d.getDay();
|
||||
break;
|
||||
}
|
||||
r.push(c);
|
||||
escape = false;
|
||||
} else {
|
||||
if (c == '%') {
|
||||
escape = true;
|
||||
} else {
|
||||
r.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r.join('');
|
||||
}
|
||||
|
||||
// To have a consistent view of time-based data independent of which time
|
||||
// zone the client happens to be in we need a date-like object independent
|
||||
// of time zones. This is done through a wrapper that only calls the UTC
|
||||
// versions of the accessor methods.
|
||||
|
||||
function makeUtcWrapper(d) {
|
||||
function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) {
|
||||
sourceObj[sourceMethod] = function() {
|
||||
return targetObj[targetMethod].apply(targetObj, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
const utc = {
|
||||
date: d,
|
||||
};
|
||||
|
||||
// support strftime, if found
|
||||
|
||||
if (d.strftime != undefined) {
|
||||
addProxyMethod(utc, 'strftime', d, 'strftime');
|
||||
}
|
||||
|
||||
addProxyMethod(utc, 'getTime', d, 'getTime');
|
||||
addProxyMethod(utc, 'setTime', d, 'setTime');
|
||||
|
||||
const props = ['Date', 'Day', 'FullYear', 'Hours', 'Milliseconds', 'Minutes', 'Month', 'Seconds'];
|
||||
|
||||
for (let p = 0; p < props.length; p++) {
|
||||
addProxyMethod(utc, 'get' + props[p], d, 'getUTC' + props[p]);
|
||||
addProxyMethod(utc, 'set' + props[p], d, 'setUTC' + props[p]);
|
||||
}
|
||||
|
||||
return utc;
|
||||
}
|
||||
|
||||
// select time zone strategy. This returns a date-like object tied to the
|
||||
// desired timezone
|
||||
|
||||
function dateGenerator(ts, opts) {
|
||||
if (opts.timezone == 'browser') {
|
||||
return new Date(ts);
|
||||
} else if (!opts.timezone || opts.timezone == 'utc') {
|
||||
return makeUtcWrapper(new Date(ts));
|
||||
}
|
||||
// } else if (typeof timezoneJS != 'undefined' && typeof timezoneJS.Date != 'undefined') {
|
||||
// const d = new timezoneJS.Date();
|
||||
// // timezone-js is fickle, so be sure to set the time zone before
|
||||
// // setting the time.
|
||||
// d.setTimezone(opts.timezone);
|
||||
// d.setTime(ts);
|
||||
// return d;
|
||||
// }
|
||||
return makeUtcWrapper(new Date(ts));
|
||||
}
|
||||
|
||||
// map of app. size of time units in milliseconds
|
||||
|
||||
const timeUnitSize = {
|
||||
second: 1000,
|
||||
minute: 60 * 1000,
|
||||
hour: 60 * 60 * 1000,
|
||||
day: 24 * 60 * 60 * 1000,
|
||||
month: 30 * 24 * 60 * 60 * 1000,
|
||||
quarter: 3 * 30 * 24 * 60 * 60 * 1000,
|
||||
year: 365.2425 * 24 * 60 * 60 * 1000,
|
||||
};
|
||||
|
||||
// the allowed tick sizes, after 1 year we use
|
||||
// an integer algorithm
|
||||
|
||||
const baseSpec = [
|
||||
[1, 'second'],
|
||||
[2, 'second'],
|
||||
[5, 'second'],
|
||||
[10, 'second'],
|
||||
[30, 'second'],
|
||||
[1, 'minute'],
|
||||
[2, 'minute'],
|
||||
[5, 'minute'],
|
||||
[10, 'minute'],
|
||||
[30, 'minute'],
|
||||
[1, 'hour'],
|
||||
[2, 'hour'],
|
||||
[4, 'hour'],
|
||||
[8, 'hour'],
|
||||
[12, 'hour'],
|
||||
[1, 'day'],
|
||||
[2, 'day'],
|
||||
[3, 'day'],
|
||||
[0.25, 'month'],
|
||||
[0.5, 'month'],
|
||||
[1, 'month'],
|
||||
[2, 'month'],
|
||||
];
|
||||
|
||||
// we don't know which variant(s) we'll need yet, but generating both is
|
||||
// cheap
|
||||
|
||||
const specMonths = baseSpec.concat([[3, 'month'], [6, 'month'], [1, 'year']]);
|
||||
const specQuarters = baseSpec.concat([[1, 'quarter'], [2, 'quarter'], [1, 'year']]);
|
||||
|
||||
function init(plot) {
|
||||
plot.hooks.processOptions.push(function(plot) {
|
||||
$.each(plot.getAxes(), function(axisName, axis) {
|
||||
const opts = axis.options;
|
||||
|
||||
if (opts.mode == 'time') {
|
||||
axis.tickGenerator = function(axis) {
|
||||
const ticks = [];
|
||||
const d = dateGenerator(axis.min, opts);
|
||||
let minSize = 0;
|
||||
|
||||
// make quarter use a possibility if quarters are
|
||||
// mentioned in either of these options
|
||||
|
||||
const spec =
|
||||
(opts.tickSize && opts.tickSize[1] === 'quarter') || (opts.minTickSize && opts.minTickSize[1] === 'quarter')
|
||||
? specQuarters
|
||||
: specMonths;
|
||||
|
||||
if (opts.minTickSize != null) {
|
||||
if (typeof opts.tickSize == 'number') {
|
||||
minSize = opts.tickSize;
|
||||
} else {
|
||||
minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < spec.length - 1; ++i) {
|
||||
if (
|
||||
axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 &&
|
||||
spec[i][0] * timeUnitSize[spec[i][1]] >= minSize
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let size = spec[i][0];
|
||||
let unit = spec[i][1];
|
||||
|
||||
// special-case the possibility of several years
|
||||
|
||||
if (unit == 'year') {
|
||||
// if given a minTickSize in years, just use it,
|
||||
// ensuring that it's an integer
|
||||
|
||||
if (opts.minTickSize != null && opts.minTickSize[1] == 'year') {
|
||||
size = Math.floor(opts.minTickSize[0]);
|
||||
} else {
|
||||
const magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10));
|
||||
const norm = axis.delta / timeUnitSize.year / magn;
|
||||
|
||||
if (norm < 1.5) {
|
||||
size = 1;
|
||||
} else if (norm < 3) {
|
||||
size = 2;
|
||||
} else if (norm < 7.5) {
|
||||
size = 5;
|
||||
} else {
|
||||
size = 10;
|
||||
}
|
||||
|
||||
size *= magn;
|
||||
}
|
||||
|
||||
// minimum size for years is 1
|
||||
|
||||
if (size < 1) {
|
||||
size = 1;
|
||||
}
|
||||
}
|
||||
|
||||
axis.tickSize = opts.tickSize || [size, unit];
|
||||
const tickSize = axis.tickSize[0];
|
||||
unit = axis.tickSize[1];
|
||||
|
||||
const step = tickSize * timeUnitSize[unit];
|
||||
|
||||
if (unit == 'second') {
|
||||
d.setSeconds(floorInBase(d.getSeconds(), tickSize));
|
||||
} else if (unit == 'minute') {
|
||||
d.setMinutes(floorInBase(d.getMinutes(), tickSize));
|
||||
} else if (unit == 'hour') {
|
||||
d.setHours(floorInBase(d.getHours(), tickSize));
|
||||
} else if (unit == 'month') {
|
||||
d.setMonth(floorInBase(d.getMonth(), tickSize));
|
||||
} else if (unit == 'quarter') {
|
||||
d.setMonth(3 * floorInBase(d.getMonth() / 3, tickSize));
|
||||
} else if (unit == 'year') {
|
||||
d.setFullYear(floorInBase(d.getFullYear(), tickSize));
|
||||
}
|
||||
|
||||
// reset smaller components
|
||||
|
||||
d.setMilliseconds(0);
|
||||
|
||||
if (step >= timeUnitSize.minute) {
|
||||
d.setSeconds(0);
|
||||
}
|
||||
if (step >= timeUnitSize.hour) {
|
||||
d.setMinutes(0);
|
||||
}
|
||||
if (step >= timeUnitSize.day) {
|
||||
d.setHours(0);
|
||||
}
|
||||
if (step >= timeUnitSize.day * 4) {
|
||||
d.setDate(1);
|
||||
}
|
||||
if (step >= timeUnitSize.month * 2) {
|
||||
d.setMonth(floorInBase(d.getMonth(), 3));
|
||||
}
|
||||
if (step >= timeUnitSize.quarter * 2) {
|
||||
d.setMonth(floorInBase(d.getMonth(), 6));
|
||||
}
|
||||
if (step >= timeUnitSize.year) {
|
||||
d.setMonth(0);
|
||||
}
|
||||
|
||||
let carry = 0;
|
||||
let v = Number.NaN;
|
||||
let prev;
|
||||
|
||||
do {
|
||||
prev = v;
|
||||
v = d.getTime();
|
||||
ticks.push(v);
|
||||
|
||||
if (unit == 'month' || unit == 'quarter') {
|
||||
if (tickSize < 1) {
|
||||
// a bit complicated - we'll divide the
|
||||
// month/quarter up but we need to take
|
||||
// care of fractions so we don't end up in
|
||||
// the middle of a day
|
||||
|
||||
d.setDate(1);
|
||||
const start = d.getTime();
|
||||
d.setMonth(d.getMonth() + (unit == 'quarter' ? 3 : 1));
|
||||
const end = d.getTime();
|
||||
d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
|
||||
carry = d.getHours();
|
||||
d.setHours(0);
|
||||
} else {
|
||||
d.setMonth(d.getMonth() + tickSize * (unit == 'quarter' ? 3 : 1));
|
||||
}
|
||||
} else if (unit == 'year') {
|
||||
d.setFullYear(d.getFullYear() + tickSize);
|
||||
} else {
|
||||
d.setTime(v + step);
|
||||
}
|
||||
} while (v < axis.max && v != prev);
|
||||
|
||||
return ticks;
|
||||
};
|
||||
|
||||
axis.tickFormatter = function(v, axis) {
|
||||
const d = dateGenerator(v, axis.options);
|
||||
|
||||
// first check global format
|
||||
|
||||
if (opts.timeformat != null) {
|
||||
return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames);
|
||||
}
|
||||
|
||||
// possibly use quarters if quarters are mentioned in
|
||||
// any of these places
|
||||
|
||||
const useQuarters =
|
||||
(axis.options.tickSize && axis.options.tickSize[1] == 'quarter') ||
|
||||
(axis.options.minTickSize && axis.options.minTickSize[1] == 'quarter');
|
||||
|
||||
const t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
|
||||
const span = axis.max - axis.min;
|
||||
const suffix = opts.twelveHourClock ? ' %p' : '';
|
||||
const hourCode = opts.twelveHourClock ? '%I' : '%H';
|
||||
let fmt;
|
||||
|
||||
if (t < timeUnitSize.minute) {
|
||||
fmt = hourCode + ':%M:%S' + suffix;
|
||||
} else if (t < timeUnitSize.day) {
|
||||
if (span < 2 * timeUnitSize.day) {
|
||||
fmt = hourCode + ':%M' + suffix;
|
||||
} else {
|
||||
fmt = '%b %d ' + hourCode + ':%M' + suffix;
|
||||
}
|
||||
} else if (t < timeUnitSize.month) {
|
||||
fmt = '%b %d';
|
||||
} else if ((useQuarters && t < timeUnitSize.quarter) || (!useQuarters && t < timeUnitSize.year)) {
|
||||
if (span < timeUnitSize.year) {
|
||||
fmt = '%b';
|
||||
} else {
|
||||
fmt = '%b %Y';
|
||||
}
|
||||
} else if (useQuarters && t < timeUnitSize.year) {
|
||||
if (span < timeUnitSize.year) {
|
||||
fmt = 'Q%q';
|
||||
} else {
|
||||
fmt = 'Q%q %Y';
|
||||
}
|
||||
} else {
|
||||
fmt = '%Y';
|
||||
}
|
||||
|
||||
const rt = formatDate(d, fmt, opts.monthNames, opts.dayNames);
|
||||
|
||||
return rt;
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$.plot.plugins.push({
|
||||
init: init,
|
||||
options: options,
|
||||
name: 'time',
|
||||
version: '1.0',
|
||||
});
|
||||
|
||||
// Time-axis support used to be in Flot core, which exposed the
|
||||
// formatDate function on the plot object. Various plugins depend
|
||||
// on the function, so we need to re-expose it here.
|
||||
|
||||
$.plot.formatDate = formatDate;
|
||||
})(window.jQuery);
|
Loading…
Reference in New Issue