Cleanup Dygraph component

pull/10616/head
Andrew Watkins 2017-08-30 23:10:38 -07:00
parent 20b249a780
commit a976a2e3a7
2 changed files with 188 additions and 185 deletions

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

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