Merge pull request #1952 from influxdata/bugfix/crosshair-remove

BUGFIX: crosshair remove
pull/10616/head
Andrew Watkins 2017-08-31 14:39:48 -07:00 committed by GitHub
commit 90bf078193
4 changed files with 207 additions and 196 deletions

View File

@ -2,10 +2,9 @@
### Bug Fixes
1. [#1886](https://github.com/influxdata/chronograf/pull/1886): Fix limit of 100 alert rules on alert rules page
1. [#1930](https://github.com/influxdata/chronograf/pull/1930): Fix graphs when y-values are constant
1. [#1947](https://github.com/influxdata/chronograf/pull/1947): Fix DataExplorer crash if field property not present on queryConfig
1. [#1951](https://github.com/influxdata/chronograf/pull/1951): Fix crosshair not being removed when user leaves graph
1. [#1943](https://github.com/influxdata/chronograf/pull/1943): Fix inability to add kapacitor from source page on fresh install
1. [#1947](https://github.com/influxdata/chronograf/pull/1947): Fix DataExplorer crash if field property not present on queryConfig
### Features
1. [#1928](https://github.com/influxdata/chronograf/pull/1928): Add prefix, suffix, scale, and other y-axis formatting

View File

@ -288,22 +288,17 @@ Dygraph.Plugins.Crosshair = (function() {
var width = e.dygraph.width_
var height = e.dygraph.height_
var xLabelPixels = 20
this.canvas_.width = width
this.canvas_.height = height
this.canvas_.height = height - xLabelPixels
this.canvas_.style.width = width + 'px' // for IE
this.canvas_.style.height = height + 'px' // for IE
this.canvas_.style.height = height - xLabelPixels + 'px' // for IE
var ctx = this.canvas_.getContext('2d')
ctx.clearRect(0, 0, width, height)
const gradient = ctx.createLinearGradient(0, 0, 0, height)
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.0)')
gradient.addColorStop(0.11, 'rgba(255, 255, 255, 1.0)')
gradient.addColorStop(0.89, 'rgba(255, 255, 255, 1.0)')
gradient.addColorStop(1, 'rgba(255, 255, 255, 0.0)')
ctx.strokeStyle = gradient
ctx.lineWidth = 1.5
ctx.strokeStyle = '#C6CAD3'
ctx.lineWidth = 1
// If graphs have different time ranges, it's possible to select a point on
// one graph that doesn't exist in another, resulting in an exception.

View File

@ -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,165 +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: this.legendFormatter,
highlightCallback: this.highlightCallback,
unhighlightCallback: this.unhighlightCallback,
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 mouseInLegendY = mouseY <= bottom && mouseY >= top
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()
@ -236,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) {
@ -254,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(),
}
@ -307,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)
@ -351,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
@ -401,6 +304,95 @@ export default class Dygraph extends Component {
return moment(timeRange).utc().format()
}
deselectCrosshair = () => {
const plugins = this.dygraph.plugins_
const crosshair = plugins.find(
({plugin}) => plugin.toString() === 'Crosshair Plugin'
)
if (!crosshair || this.props.isBarGraph) {
return
}
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() {
const {
legend,
@ -413,7 +405,7 @@ export default class Dygraph extends Component {
} = this.state
return (
<div className="dygraph-child">
<div className="dygraph-child" onMouseLeave={this.deselectCrosshair}>
<DygraphLegend
{...legend}
sortType={sortType}

View File

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