Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ReactUI: adopt grafana flot fix for stacked graphs #6603

Merged
merged 9 commits into from Jan 14, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 1 addition & 2 deletions web/ui/react-app/package.json
Expand Up @@ -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",
Expand All @@ -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}\"",
Expand Down
2 changes: 1 addition & 1 deletion web/ui/react-app/src/graph/Graph.test.tsx
Expand Up @@ -104,7 +104,7 @@ describe('Graph', () => {
chartData: [
{
color: 'rgb(237,194,64)',
data: [[1572128592000, 0]],
data: [[1572128592000, null]],
index: 0,
labels: {},
},
Expand Down
9 changes: 4 additions & 5 deletions web/ui/react-app/src/graph/Graph.tsx
Expand Up @@ -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 {
Expand Down
8 changes: 4 additions & 4 deletions web/ui/react-app/src/graph/GraphHelpers.test.ts
@@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
24 changes: 7 additions & 17 deletions web/ui/react-app/src/graph/GraphHelpers.ts
Expand Up @@ -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) => {
Expand All @@ -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]);
}
}

Expand All @@ -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;
};
191 changes: 191 additions & 0 deletions web/ui/react-app/src/vendor/flot/jquery.flot.crosshair.js
@@ -0,0 +1,191 @@
/* 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.
*/

/**
*
* 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 REPRODUCED BELOW. ADDITIONAL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd put this new notice header at the very top. Then the word "BELOW" also makes sense :)

Not sure why it'd all be capitalized? At least the URLs are broken through the capitalization now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And: actually my "as reproduced below" wasn't correct, since the original files don't contain a full reproduction of the MIT license. So "as reproduced below" -> "as stated below".

* CHANGES HAVE BEEN CONTRIBUTED TO THE GRAFANA FORK UNDER AN APACHE 2 LICENSE, SEE
* HTTPS://GITHUB.COM/GRAFANA/GRAFANA/BLOB/MASTER/LICENSE.
*
*/

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