diff --git a/ui/src/shared/components/Dygraph.js b/ui/src/shared/components/Dygraph.js index 732d738b1a..68f3296a80 100644 --- a/ui/src/shared/components/Dygraph.js +++ b/ui/src/shared/components/Dygraph.js @@ -7,22 +7,20 @@ import moment from 'moment' import Dygraphs from 'src/external/dygraph' import getRange from 'shared/parsing/getRangeForDygraph' - -import {DISPLAY_OPTIONS} from 'src/dashboards/constants' -import {LINE_COLORS, multiColumnBarPlotter} from 'src/shared/graphs/helpers' import DygraphLegend from 'src/shared/components/DygraphLegend' +import {DISPLAY_OPTIONS} from 'src/dashboards/constants' import {buildDefaultYLabel} from 'shared/presenters' import {numberValueFormatter} from 'src/utils/formatting' - -const {LINEAR, LOG, BASE_10} = DISPLAY_OPTIONS -const labelWidth = 60 -const avgCharPixels = 7 - -const hasherino = (str, len) => - str - .split('') - .map(char => char.charCodeAt(0)) - .reduce((hash, code) => hash + code, 0) % len +import { + OPTIONS, + LINE_COLORS, + LABEL_WIDTH, + CHAR_PIXELS, + barPlotter, + hasherino, + highlightSeriesOpts, +} from 'src/shared/graphs/helpers' +const {LINEAR, LOG, BASE_10, BASE_2} = DISPLAY_OPTIONS export default class Dygraph extends Component { constructor(props) { @@ -43,166 +41,59 @@ export default class Dygraph extends Component { } componentDidMount() { - const timeSeries = this.getTimeSeries() - // dygraphSeries is a legend label and its corresponding y-axis e.g. {legendLabel1: 'y', legendLabel2: 'y2'}; const { axes: {y, y2}, - dygraphSeries, ruleValues, - overrideLineColors, - isGraphFilled, + isGraphFilled: fillGraph, isBarGraph, options, - onZoom, } = this.props + const timeSeries = this.getTimeSeries() const graphRef = this.graphRef - const legendRef = this.legendRef - const finalLineColors = [...(overrideLineColors || LINE_COLORS)] - const hashColorDygraphSeries = {} - const {length} = finalLineColors - - for (const seriesName in dygraphSeries) { - const series = dygraphSeries[seriesName] - const hashIndex = hasherino(seriesName, length) - const color = finalLineColors[hashIndex] - hashColorDygraphSeries[seriesName] = {...series, color} - } - - const axisLabelWidth = - labelWidth + - y.prefix.length * avgCharPixels + - y.suffix.length * avgCharPixels - - const defaultOptions = { - plugins: isBarGraph - ? [] - : [ - new Dygraphs.Plugins.Crosshair({ - direction: 'vertical', - }), - ], + let defaultOptions = { + fillGraph, logscale: y.scale === LOG, - labelsSeparateLines: false, - labelsKMB: true, - rightGap: 0, - highlightSeriesBackgroundAlpha: 1.0, - highlightSeriesBackgroundColor: 'rgb(41, 41, 51)', - fillGraph: isGraphFilled, - axisLineWidth: 2, - gridLineWidth: 1, - highlightCircleSize: isBarGraph ? 0 : 3, - animatedZooms: true, - hideOverlayOnMouseOut: false, - colors: finalLineColors, - series: hashColorDygraphSeries, + colors: this.getLineColors, + series: this.hashColorDygraphSeries(), + legendFormatter: legend => this.legendFormatter(legend), + highlightCallback: e => this.highlightCallback(e), + unhighlightCallback: e => this.unhighlightCallback(e), + plugins: [new Dygraphs.Plugins.Crosshair({direction: 'vertical'})], axes: { y: { valueRange: getRange(timeSeries, y.bounds, ruleValues), axisLabelFormatter: (yval, __, opts) => numberValueFormatter(yval, opts, y.prefix, y.suffix), - axisLabelWidth, - labelsKMB: y.base === '10', - labelsKMG2: y.base === '2', + axisLabelWidth: this.getLabelWidth(), + labelsKMB: y.base === BASE_10, + labelsKMG2: y.base === BASE_2, }, y2: { valueRange: getRange(timeSeries, y2.bounds), }, }, - highlightSeriesOpts: { - strokeWidth: 2, - highlightCircleSize: isBarGraph ? 0 : 5, - }, - legendFormatter: legend => { - if (!legend.x) { - return '' - } - - const {state: {legend: prevLegend}} = this - const highlighted = legend.series.find(s => s.isHighlighted) - const prevHighlighted = prevLegend.series.find(s => s.isHighlighted) - - const yVal = highlighted && highlighted.y - const prevY = prevHighlighted && prevHighlighted.y - - if (legend.x === prevLegend.x && yVal === prevY) { - return '' - } - - this.setState({legend}) - return '' - }, - highlightCallback: e => { - // Move the Legend on hover - const graphRect = graphRef.getBoundingClientRect() - const legendRect = legendRef.getBoundingClientRect() - - const graphWidth = graphRect.width + 32 // Factoring in padding from parent - const graphHeight = graphRect.height - const graphBottom = graphRect.bottom - const legendWidth = legendRect.width - const legendHeight = legendRect.height - const screenHeight = window.innerHeight - const legendMaxLeft = graphWidth - legendWidth / 2 - const trueGraphX = e.pageX - graphRect.left - - let legendLeft = trueGraphX - - // Enforcing max & min legend offsets - if (trueGraphX < legendWidth / 2) { - legendLeft = legendWidth / 2 - } else if (trueGraphX > legendMaxLeft) { - legendLeft = legendMaxLeft - } - - // Disallow screen overflow of legend - const isLegendBottomClipped = graphBottom + legendHeight > screenHeight - - const legendTop = isLegendBottomClipped - ? graphHeight + 8 - legendHeight - : graphHeight + 8 - - legendRef.style.left = `${legendLeft}px` - legendRef.style.top = `${legendTop}px` - - this.setState({isHidden: false}) - }, - unhighlightCallback: e => { - const {top, bottom, left, right} = legendRef.getBoundingClientRect() - - const mouseY = e.clientY - const mouseX = e.clientX - - const mouseBuffer = 5 - const mouseInLegendY = mouseY <= bottom && mouseY >= top - mouseBuffer - const mouseInLegendX = mouseX <= right && mouseX >= left - const isMouseHoveringLegend = mouseInLegendY && mouseInLegendX - - if (!isMouseHoveringLegend) { - this.setState({isHidden: true}) - - if (!this.visibility().find(bool => bool === true)) { - this.setState({filterText: ''}) - } - } - }, - zoomCallback: (lower, upper) => { - if (this.dygraph.isZoomed() === false) { - return onZoom(null, null) - } - - onZoom(this.formatTimeRange(lower), this.formatTimeRange(upper)) - }, + highlightSeriesOpts, + zoomCallback: (lower, upper) => this.handleZoom(lower, upper), } if (isBarGraph) { - defaultOptions.plotter = multiColumnBarPlotter + defaultOptions = { + ...defaultOptions, + plotter: barPlotter, + plugins: [], + highlightSeriesOpts: { + ...highlightSeriesOpts, + highlightCircleSize: 0, + }, + } } this.dygraph = new Dygraphs(graphRef, timeSeries, { ...defaultOptions, ...options, + ...OPTIONS, }) const {w} = this.dygraph.getArea() @@ -237,15 +128,7 @@ export default class Dygraph extends Component { } componentDidUpdate() { - const { - labels, - axes: {y, y2}, - options, - dygraphSeries, - ruleValues, - isBarGraph, - overrideLineColors, - } = this.props + const {labels, axes: {y, y2}, options, ruleValues, isBarGraph} = this.props const dygraph = this.dygraph if (!dygraph) { @@ -255,48 +138,29 @@ export default class Dygraph extends Component { } const timeSeries = this.getTimeSeries() - const ylabel = this.getLabel('y') - const finalLineColors = [...(overrideLineColors || LINE_COLORS)] - - const hashColorDygraphSeries = {} - const {length} = finalLineColors - - for (const seriesName in dygraphSeries) { - const series = dygraphSeries[seriesName] - const hashIndex = hasherino(seriesName, length) - const color = finalLineColors[hashIndex] - hashColorDygraphSeries[seriesName] = {...series, color} - } - - const axisLabelWidth = - labelWidth + - y.prefix.length * avgCharPixels + - y.suffix.length * avgCharPixels const updateOptions = { + ...options, labels, + ylabel: this.getLabel('y'), file: timeSeries, - ylabel, logscale: y.scale === LOG, axes: { y: { valueRange: getRange(timeSeries, y.bounds, ruleValues), axisLabelFormatter: (yval, __, opts) => numberValueFormatter(yval, opts, y.prefix, y.suffix), - axisLabelWidth, - labelsKMB: y.base === '10', - labelsKMG2: y.base === '2', + axisLabelWidth: this.getLabelWidth(), + labelsKMB: y.base === BASE_10, + labelsKMG2: y.base === BASE_2, }, y2: { valueRange: getRange(timeSeries, y2.bounds), }, }, - stepPlot: options.stepPlot, - stackedGraph: options.stackedGraph, - underlayCallback: options.underlayCallback, - colors: finalLineColors, - series: hashColorDygraphSeries, - plotter: isBarGraph ? multiColumnBarPlotter : null, + colors: this.getLineColors, + series: this.hashColorDygraphSeries(), + plotter: isBarGraph ? barPlotter : null, visibility: this.visibility(), } @@ -308,6 +172,31 @@ export default class Dygraph extends Component { this.props.setResolution(w) } + handleZoom = (lower, upper) => { + const {onZoom} = this.props + + if (this.dygraph.isZoomed() === false) { + return onZoom(null, null) + } + + onZoom(this.formatTimeRange(lower), this.formatTimeRange(upper)) + } + + hashColorDygraphSeries = () => { + const {dygraphSeries} = this.props + const colors = this.getLineColors() + const hashColorDygraphSeries = {} + + for (const seriesName in dygraphSeries) { + const series = dygraphSeries[seriesName] + const hashIndex = hasherino(seriesName, colors.length) + const color = colors[hashIndex] + hashColorDygraphSeries[seriesName] = {...series, color} + } + + return hashColorDygraphSeries + } + sync = () => { if (!this.state.isSynced) { this.props.synchronizer(this.dygraph) @@ -352,6 +241,19 @@ export default class Dygraph extends Component { } } + getLineColors = () => { + return [...(this.props.overrideLineColors || LINE_COLORS)] + } + + getLabelWidth = () => { + const {axes: {y}} = this.props + return ( + LABEL_WIDTH + + y.prefix.length * CHAR_PIXELS + + y.suffix.length * CHAR_PIXELS + ) + } + visibility = () => { const timeSeries = this.getTimeSeries() const {filterText, legend} = this.state @@ -404,15 +306,91 @@ export default class Dygraph extends Component { deselectCrosshair = () => { const plugins = this.dygraph.plugins_ - const {plugin: crosshair} = plugins.find( + const crosshair = plugins.find( ({plugin}) => plugin.toString() === 'Crosshair Plugin' ) - if (!crosshair) { + if (!crosshair || this.props.isBarGraph) { return } - crosshair.deselect() + crosshair.plugin.deselect() + } + + unhighlightCallback = e => { + const {top, bottom, left, right} = this.legendRef.getBoundingClientRect() + + const mouseY = e.clientY + const mouseX = e.clientX + + const mouseBuffer = 5 + const mouseInLegendY = mouseY <= bottom && mouseY >= top - mouseBuffer + const mouseInLegendX = mouseX <= right && mouseX >= left + const isMouseHoveringLegend = mouseInLegendY && mouseInLegendX + + if (!isMouseHoveringLegend) { + this.setState({isHidden: true}) + + if (!this.visibility().find(bool => bool === true)) { + this.setState({filterText: ''}) + } + } + } + + highlightCallback = e => { + // Move the Legend on hover + const graphRect = this.graphRef.getBoundingClientRect() + const legendRect = this.legendRef.getBoundingClientRect() + + const graphWidth = graphRect.width + 32 // Factoring in padding from parent + const graphHeight = graphRect.height + const graphBottom = graphRect.bottom + const legendWidth = legendRect.width + const legendHeight = legendRect.height + const screenHeight = window.innerHeight + const legendMaxLeft = graphWidth - legendWidth / 2 + const trueGraphX = e.pageX - graphRect.left + + let legendLeft = trueGraphX + + // Enforcing max & min legend offsets + if (trueGraphX < legendWidth / 2) { + legendLeft = legendWidth / 2 + } else if (trueGraphX > legendMaxLeft) { + legendLeft = legendMaxLeft + } + + // Disallow screen overflow of legend + const isLegendBottomClipped = graphBottom + legendHeight > screenHeight + + const legendTop = isLegendBottomClipped + ? graphHeight + 8 - legendHeight + : graphHeight + 8 + + this.legendRef.style.left = `${legendLeft}px` + this.legendRef.style.top = `${legendTop}px` + + this.setState({isHidden: false}) + } + + legendFormatter = legend => { + if (!legend.x) { + return '' + } + + const {state: {legend: prevLegend}} = this + const highlighted = legend.series.find(s => s.isHighlighted) + const prevHighlighted = prevLegend.series.find(s => s.isHighlighted) + + const yVal = highlighted && highlighted.y + const prevY = prevHighlighted && prevHighlighted.y + + if (legend.x === prevLegend.x && yVal === prevY) { + return '' + } + + this.setState({legend}) + return '' } render() { diff --git a/ui/src/shared/graphs/helpers.js b/ui/src/shared/graphs/helpers.js index 33786c8b5e..c989112627 100644 --- a/ui/src/shared/graphs/helpers.js +++ b/ui/src/shared/graphs/helpers.js @@ -27,7 +27,7 @@ export const darkenColor = colorStr => { } // Bar Graph code below is adapted from http://dygraphs.com/tests/plotters.html -export const multiColumnBarPlotter = e => { +export const barPlotter = e => { // We need to handle all the series simultaneously. if (e.seriesIndex !== 0) { return @@ -99,3 +99,28 @@ export const multiColumnBarPlotter = e => { } } } + +export const OPTIONS = { + rightGap: 0, + axisLineWidth: 2, + gridLineWidth: 1, + animatedZooms: true, + labelsSeparateLines: false, + hideOverlayOnMouseOut: false, + highlightSeriesBackgroundAlpha: 1.0, + highlightSeriesBackgroundColor: 'rgb(41, 41, 51)', +} + +export const highlightSeriesOpts = { + strokeWidth: 2, + highlightCircleSize: 5, +} + +export const hasherino = (str, len) => + str + .split('') + .map(char => char.charCodeAt(0)) + .reduce((hash, code) => hash + code, 0) % len + +export const LABEL_WIDTH = 60 +export const CHAR_PIXELS = 7