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
Boyko 2020-01-14 17:12:08 +02:00 committed by Julius Volz
parent 3b9304d22e
commit e12e5ecc8f
9 changed files with 4307 additions and 29 deletions

View File

@ -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}\"",

View File

@ -104,7 +104,7 @@ describe('Graph', () => {
chartData: [
{
color: 'rgb(237,194,64)',
data: [[1572128592000, 0]],
data: [[1572128592000, null]],
index: 0,
labels: {},
},

View File

@ -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 {

View File

@ -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', () => {

View File

@ -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;
return isNaN(val) ? null : val;
};

View File

@ -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

View File

@ -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);

View File

@ -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);