Merge pull request #1679 from influxdata/get-legend-rekt
Update legend functionalitypull/10616/head
commit
0a80cac953
|
@ -7,104 +7,34 @@ import _ from 'lodash'
|
||||||
import Dygraphs from 'src/external/dygraph'
|
import Dygraphs from 'src/external/dygraph'
|
||||||
import getRange from 'shared/parsing/getRangeForDygraph'
|
import getRange from 'shared/parsing/getRangeForDygraph'
|
||||||
|
|
||||||
const LINE_COLORS = [
|
import {LINE_COLORS, multiColumnBarPlotter} from 'src/shared/graphs/helpers'
|
||||||
'#00C9FF',
|
import DygraphLegend from 'src/shared/components/DygraphLegend'
|
||||||
'#9394FF',
|
|
||||||
'#4ED8A0',
|
|
||||||
'#ff0054',
|
|
||||||
'#ffcc00',
|
|
||||||
'#33aa99',
|
|
||||||
'#9dfc5d',
|
|
||||||
'#92bcc3',
|
|
||||||
'#ca96fb',
|
|
||||||
'#ff00f0',
|
|
||||||
'#38b94a',
|
|
||||||
'#3844b9',
|
|
||||||
'#a0725b',
|
|
||||||
]
|
|
||||||
|
|
||||||
const darkenColor = colorStr => {
|
|
||||||
// Defined in dygraph-utils.js
|
|
||||||
const color = Dygraphs.toRGB_(colorStr)
|
|
||||||
color.r = Math.floor((255 + color.r) / 2)
|
|
||||||
color.g = Math.floor((255 + color.g) / 2)
|
|
||||||
color.b = Math.floor((255 + color.b) / 2)
|
|
||||||
return `rgb(${color.r},${color.g},${color.b})`
|
|
||||||
}
|
|
||||||
// Bar Graph code below is from http://dygraphs.com/tests/plotters.html
|
|
||||||
const multiColumnBarPlotter = e => {
|
|
||||||
// We need to handle all the series simultaneously.
|
|
||||||
if (e.seriesIndex !== 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const g = e.dygraph
|
|
||||||
const ctx = e.drawingContext
|
|
||||||
const sets = e.allSeriesPoints
|
|
||||||
const yBottom = e.dygraph.toDomYCoord(0)
|
|
||||||
|
|
||||||
// Find the minimum separation between x-values.
|
|
||||||
// This determines the bar width.
|
|
||||||
let minSep = Infinity
|
|
||||||
for (let j = 0; j < sets.length; j++) {
|
|
||||||
const points = sets[j]
|
|
||||||
for (let i = 1; i < points.length; i++) {
|
|
||||||
const sep = points[i].canvasx - points[i - 1].canvasx
|
|
||||||
if (sep < minSep) {
|
|
||||||
minSep = sep
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const barWidth = Math.floor(2.0 / 3 * minSep)
|
|
||||||
|
|
||||||
const fillColors = []
|
|
||||||
const strokeColors = g.getColors()
|
|
||||||
for (let i = 0; i < strokeColors.length; i++) {
|
|
||||||
fillColors.push(darkenColor(strokeColors[i]))
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let j = 0; j < sets.length; j++) {
|
|
||||||
ctx.fillStyle = fillColors[j]
|
|
||||||
ctx.strokeStyle = strokeColors[j]
|
|
||||||
for (let i = 0; i < sets[j].length; i++) {
|
|
||||||
const p = sets[j][i]
|
|
||||||
const centerX = p.canvasx
|
|
||||||
const xLeft = sets.length === 1
|
|
||||||
? centerX - barWidth / 2
|
|
||||||
: centerX - barWidth / 2 * (1 - j / (sets.length - 1))
|
|
||||||
|
|
||||||
ctx.fillRect(
|
|
||||||
xLeft,
|
|
||||||
p.canvasy,
|
|
||||||
barWidth / sets.length,
|
|
||||||
yBottom - p.canvasy
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx.strokeRect(
|
|
||||||
xLeft,
|
|
||||||
p.canvasy,
|
|
||||||
barWidth / sets.length,
|
|
||||||
yBottom - p.canvasy
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Dygraph extends Component {
|
export default class Dygraph extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
|
legend: {
|
||||||
|
x: null,
|
||||||
|
series: [],
|
||||||
|
},
|
||||||
|
sortType: '',
|
||||||
|
filterText: '',
|
||||||
isSynced: false,
|
isSynced: false,
|
||||||
|
isHidden: true,
|
||||||
|
isAscending: true,
|
||||||
|
isSnipped: false,
|
||||||
|
isFilterVisible: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// optional workaround for dygraph.updateOptions breaking legends
|
|
||||||
// a la http://stackoverflow.com/questions/38371876/dygraph-dynamic-update-legend-values-disappear
|
|
||||||
// this.lastMouseMoveEvent = null
|
|
||||||
// this.isMouseOverGraph = false
|
|
||||||
|
|
||||||
this.getTimeSeries = ::this.getTimeSeries
|
|
||||||
this.sync = ::this.sync
|
this.sync = ::this.sync
|
||||||
|
this.getTimeSeries = ::this.getTimeSeries
|
||||||
|
this.handleSortLegend = ::this.handleSortLegend
|
||||||
|
this.handleLegendInputChange = ::this.handleLegendInputChange
|
||||||
|
this.handleSnipLabel = ::this.handleSnipLabel
|
||||||
|
this.handleHideLegend = ::this.handleHideLegend
|
||||||
|
this.handleToggleFilter = ::this.handleToggleFilter
|
||||||
|
this.visibility = ::this.visibility
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -133,8 +63,8 @@ export default class Dygraph extends Component {
|
||||||
options,
|
options,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const graphContainerNode = this.graphContainer
|
const graphRef = this.graphRef
|
||||||
const legendContainerNode = this.legendContainer
|
const legendRef = this.legendRef
|
||||||
let finalLineColors = overrideLineColors
|
let finalLineColors = overrideLineColors
|
||||||
|
|
||||||
if (finalLineColors === null) {
|
if (finalLineColors === null) {
|
||||||
|
@ -148,7 +78,6 @@ export default class Dygraph extends Component {
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
labelsSeparateLines: false,
|
labelsSeparateLines: false,
|
||||||
labelsDiv: legendContainerNode,
|
|
||||||
labelsKMB: true,
|
labelsKMB: true,
|
||||||
rightGap: 0,
|
rightGap: 0,
|
||||||
highlightSeriesBackgroundAlpha: 1.0,
|
highlightSeriesBackgroundAlpha: 1.0,
|
||||||
|
@ -158,6 +87,7 @@ export default class Dygraph extends Component {
|
||||||
gridLineWidth: 1,
|
gridLineWidth: 1,
|
||||||
highlightCircleSize: 3,
|
highlightCircleSize: 3,
|
||||||
animatedZooms: true,
|
animatedZooms: true,
|
||||||
|
hideOverlayOnMouseOut: false,
|
||||||
colors: finalLineColors,
|
colors: finalLineColors,
|
||||||
series: dygraphSeries,
|
series: dygraphSeries,
|
||||||
axes: {
|
axes: {
|
||||||
|
@ -172,20 +102,30 @@ export default class Dygraph extends Component {
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
highlightCircleSize: 5,
|
highlightCircleSize: 5,
|
||||||
},
|
},
|
||||||
unhighlightCallback: () => {
|
legendFormatter: legend => {
|
||||||
legendContainerNode.className = 'container--dygraph-legend hidden' // hide
|
if (!legend.x) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
// part of optional workaround for preventing updateOptions from breaking legend
|
const {state: {legend: prevLegend}} = this
|
||||||
// this.isMouseOverGraph = false
|
const highlighted = legend.series.find(s => s.isHighlighted)
|
||||||
|
const prevHighlighted = prevLegend.series.find(s => s.isHighlighted)
|
||||||
|
|
||||||
|
const y = highlighted && highlighted.y
|
||||||
|
const prevY = prevHighlighted && prevHighlighted.y
|
||||||
|
|
||||||
|
if (legend.x === prevLegend.x && y === prevY) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({legend})
|
||||||
|
return ''
|
||||||
},
|
},
|
||||||
highlightCallback: e => {
|
highlightCallback: e => {
|
||||||
// don't make visible yet, but render on DOM to capture position for calcs
|
|
||||||
legendContainerNode.style.visibility = 'hidden'
|
|
||||||
legendContainerNode.className = 'container--dygraph-legend'
|
|
||||||
|
|
||||||
// Move the Legend on hover
|
// Move the Legend on hover
|
||||||
const graphRect = graphContainerNode.getBoundingClientRect()
|
const graphRect = graphRef.getBoundingClientRect()
|
||||||
const legendRect = legendContainerNode.getBoundingClientRect()
|
const legendRect = legendRef.getBoundingClientRect()
|
||||||
|
|
||||||
const graphWidth = graphRect.width + 32 // Factoring in padding from parent
|
const graphWidth = graphRect.width + 32 // Factoring in padding from parent
|
||||||
const graphHeight = graphRect.height
|
const graphHeight = graphRect.height
|
||||||
const graphBottom = graphRect.bottom
|
const graphBottom = graphRect.bottom
|
||||||
|
@ -211,16 +151,28 @@ export default class Dygraph extends Component {
|
||||||
? graphHeight + 8 - legendHeight
|
? graphHeight + 8 - legendHeight
|
||||||
: graphHeight + 8
|
: graphHeight + 8
|
||||||
|
|
||||||
legendContainerNode.style.visibility = 'visible' // show
|
legendRef.style.left = `${legendLeft}px`
|
||||||
legendContainerNode.style.left = `${legendLeft}px`
|
legendRef.style.top = `${legendTop}px`
|
||||||
legendContainerNode.style.top = `${legendTop}px`
|
|
||||||
|
|
||||||
// part of optional workaround for preventing updateOptions from breaking legend
|
this.setState({isHidden: false})
|
||||||
// this.isMouseOverGraph = true
|
|
||||||
// this.lastMouseMoveEvent = e
|
|
||||||
},
|
},
|
||||||
drawCallback: () => {
|
unhighlightCallback: e => {
|
||||||
legendContainerNode.className = 'container--dygraph-legend hidden' // hide
|
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: ''})
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,7 +180,7 @@ export default class Dygraph extends Component {
|
||||||
defaultOptions.plotter = multiColumnBarPlotter
|
defaultOptions.plotter = multiColumnBarPlotter
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dygraph = new Dygraphs(graphContainerNode, timeSeries, {
|
this.dygraph = new Dygraphs(graphRef, timeSeries, {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
@ -264,6 +216,22 @@ export default class Dygraph extends Component {
|
||||||
return shallowCompare(this, nextProps, nextState)
|
return shallowCompare(this, nextProps, nextState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
visibility() {
|
||||||
|
const timeSeries = this.getTimeSeries()
|
||||||
|
const {filterText, legend} = this.state
|
||||||
|
const series = _.get(timeSeries, '0', [])
|
||||||
|
const numSeries = series.length
|
||||||
|
return Array(numSeries ? numSeries - 1 : numSeries)
|
||||||
|
.fill(true)
|
||||||
|
.map((s, i) => {
|
||||||
|
if (!legend.series[i]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!legend.series[i].label.match(filterText)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
const {
|
const {
|
||||||
labels,
|
labels,
|
||||||
|
@ -273,6 +241,7 @@ export default class Dygraph extends Component {
|
||||||
ruleValues,
|
ruleValues,
|
||||||
isBarGraph,
|
isBarGraph,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const dygraph = this.dygraph
|
const dygraph = this.dygraph
|
||||||
if (!dygraph) {
|
if (!dygraph) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -281,11 +250,7 @@ export default class Dygraph extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeSeries = this.getTimeSeries()
|
const timeSeries = this.getTimeSeries()
|
||||||
|
const updateOptions = {
|
||||||
const legendContainerNode = this.legendContainer
|
|
||||||
legendContainerNode.className = 'container--dygraph-legend hidden' // hide
|
|
||||||
|
|
||||||
dygraph.updateOptions({
|
|
||||||
labels,
|
labels,
|
||||||
file: timeSeries,
|
file: timeSeries,
|
||||||
axes: {
|
axes: {
|
||||||
|
@ -301,12 +266,10 @@ export default class Dygraph extends Component {
|
||||||
underlayCallback: options.underlayCallback,
|
underlayCallback: options.underlayCallback,
|
||||||
series: dygraphSeries,
|
series: dygraphSeries,
|
||||||
plotter: isBarGraph ? multiColumnBarPlotter : null,
|
plotter: isBarGraph ? multiColumnBarPlotter : null,
|
||||||
})
|
visibility: this.visibility(),
|
||||||
// part of optional workaround for preventing updateOptions from breaking legend
|
}
|
||||||
// if (this.lastMouseMoveEvent) {
|
|
||||||
// dygraph.mouseMove_(this.lastMouseMoveEvent)
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
dygraph.updateOptions(updateOptions)
|
||||||
dygraph.resize()
|
dygraph.resize()
|
||||||
const {w} = this.dygraph.getArea()
|
const {w} = this.dygraph.getArea()
|
||||||
this.props.setResolution(w)
|
this.props.setResolution(w)
|
||||||
|
@ -319,21 +282,77 @@ export default class Dygraph extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSortLegend(sortType) {
|
||||||
|
this.setState({sortType, isAscending: !this.state.isAscending})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLegendInputChange(e) {
|
||||||
|
this.setState({filterText: e.target.value})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSnipLabel() {
|
||||||
|
this.setState({isSnipped: !this.state.isSnipped})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToggleFilter() {
|
||||||
|
this.setState({
|
||||||
|
isFilterVisible: !this.state.isFilterVisible,
|
||||||
|
filterText: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHideLegend(e) {
|
||||||
|
const {top, bottom, left, right} = this.graphRef.getBoundingClientRect()
|
||||||
|
|
||||||
|
const mouseY = e.clientY
|
||||||
|
const mouseX = e.clientX
|
||||||
|
|
||||||
|
const mouseInGraphY = mouseY <= bottom && mouseY >= top
|
||||||
|
const mouseInGraphX = mouseX <= right && mouseX >= left
|
||||||
|
const isMouseHoveringGraph = mouseInGraphY && mouseInGraphX
|
||||||
|
|
||||||
|
if (!isMouseHoveringGraph) {
|
||||||
|
this.setState({isHidden: true})
|
||||||
|
if (!this.visibility().find(bool => bool === true)) {
|
||||||
|
this.setState({filterText: ''})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
legend,
|
||||||
|
filterText,
|
||||||
|
isAscending,
|
||||||
|
sortType,
|
||||||
|
isHidden,
|
||||||
|
isSnipped,
|
||||||
|
isFilterVisible,
|
||||||
|
} = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dygraph-child">
|
<div className="dygraph-child">
|
||||||
<div
|
<DygraphLegend
|
||||||
ref={r => {
|
{...legend}
|
||||||
this.graphContainer = r
|
sortType={sortType}
|
||||||
}}
|
onHide={this.handleHideLegend}
|
||||||
style={this.props.containerStyle}
|
isHidden={isHidden}
|
||||||
className="dygraph-child-container"
|
isFilterVisible={isFilterVisible}
|
||||||
|
isSnipped={isSnipped}
|
||||||
|
filterText={filterText}
|
||||||
|
isAscending={isAscending}
|
||||||
|
onSnip={this.handleSnipLabel}
|
||||||
|
onSort={this.handleSortLegend}
|
||||||
|
legendRef={el => (this.legendRef = el)}
|
||||||
|
onInputChange={this.handleLegendInputChange}
|
||||||
|
onToggleFilter={this.handleToggleFilter}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref={r => {
|
ref={r => {
|
||||||
this.legendContainer = r
|
this.graphRef = r
|
||||||
}}
|
}}
|
||||||
className={'container--dygraph-legend hidden'}
|
style={this.props.containerStyle}
|
||||||
|
className="dygraph-child-container"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
import React, {PropTypes} from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
|
||||||
|
const removeMeasurement = (label = '') => {
|
||||||
|
const [measurement] = label.match(/^(.*)[.]/g) || ['']
|
||||||
|
return label.replace(measurement, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const DygraphLegend = ({
|
||||||
|
series,
|
||||||
|
onSort,
|
||||||
|
onSnip,
|
||||||
|
onHide,
|
||||||
|
isHidden,
|
||||||
|
isFilterVisible,
|
||||||
|
isSnipped,
|
||||||
|
sortType,
|
||||||
|
legendRef,
|
||||||
|
filterText,
|
||||||
|
isAscending,
|
||||||
|
onInputChange,
|
||||||
|
onToggleFilter,
|
||||||
|
xHTML,
|
||||||
|
}) => {
|
||||||
|
const sorted = _.sortBy(
|
||||||
|
series,
|
||||||
|
({y, label}) => (sortType === 'numeric' ? y : label)
|
||||||
|
)
|
||||||
|
const ordered = isAscending ? sorted : sorted.reverse()
|
||||||
|
const filtered = ordered.filter(s => s.label.match(filterText))
|
||||||
|
const hidden = isHidden ? 'hidden' : ''
|
||||||
|
|
||||||
|
const renderSortAlpha = (
|
||||||
|
<div
|
||||||
|
className={classnames('sort-btn btn btn-sm btn-square', {
|
||||||
|
'btn-primary': sortType !== 'numeric',
|
||||||
|
'btn-default': sortType === 'numeric',
|
||||||
|
'sort-btn--asc': isAscending && sortType !== 'numeric',
|
||||||
|
'sort-btn--desc': !isAscending && sortType !== 'numeric',
|
||||||
|
})}
|
||||||
|
onClick={() => onSort('alphabetic')}
|
||||||
|
>
|
||||||
|
<div className="sort-btn--arrow" />
|
||||||
|
<div className="sort-btn--top">A</div>
|
||||||
|
<div className="sort-btn--bottom">Z</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
const renderSortNum = (
|
||||||
|
<button
|
||||||
|
className={classnames('sort-btn btn btn-sm btn-square', {
|
||||||
|
'btn-primary': sortType === 'numeric',
|
||||||
|
'btn-default': sortType !== 'numeric',
|
||||||
|
'sort-btn--asc': isAscending && sortType === 'numeric',
|
||||||
|
'sort-btn--desc': !isAscending && sortType === 'numeric',
|
||||||
|
})}
|
||||||
|
onClick={() => onSort('numeric')}
|
||||||
|
>
|
||||||
|
<div className="sort-btn--arrow" />
|
||||||
|
<div className="sort-btn--top">0</div>
|
||||||
|
<div className="sort-btn--bottom">9</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`dygraph-legend ${hidden}`}
|
||||||
|
ref={legendRef}
|
||||||
|
onMouseLeave={onHide}
|
||||||
|
>
|
||||||
|
<div className="dygraph-legend--header">
|
||||||
|
<div className="dygraph-legend--timestamp">{xHTML}</div>
|
||||||
|
{renderSortAlpha}
|
||||||
|
{renderSortNum}
|
||||||
|
<button
|
||||||
|
className={classnames('btn btn-square btn-sm', {
|
||||||
|
'btn-default': !isFilterVisible,
|
||||||
|
'btn-primary': isFilterVisible,
|
||||||
|
})}
|
||||||
|
onClick={onToggleFilter}
|
||||||
|
>
|
||||||
|
<span className="icon search" />
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-default btn-sm" onClick={onSnip}>
|
||||||
|
Snip
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{isFilterVisible
|
||||||
|
? <input
|
||||||
|
className="dygraph-legend--filter form-control input-sm"
|
||||||
|
type="text"
|
||||||
|
value={filterText}
|
||||||
|
onChange={onInputChange}
|
||||||
|
placeholder="Filter items..."
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
: null}
|
||||||
|
<div className="dygraph-legend--divider" />
|
||||||
|
<div className="dygraph-legend--contents">
|
||||||
|
{filtered.map(({label, color, yHTML, isHighlighted}) => {
|
||||||
|
const seriesClass = isHighlighted
|
||||||
|
? 'dygraph-legend--row highlight'
|
||||||
|
: 'dygraph-legend--row'
|
||||||
|
return (
|
||||||
|
<div key={label + color} className={seriesClass}>
|
||||||
|
<span style={{color}}>
|
||||||
|
{isSnipped ? removeMeasurement(label) : label}
|
||||||
|
</span>
|
||||||
|
<figure>{yHTML || 'no value'}</figure>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {arrayOf, bool, func, number, shape, string} = PropTypes
|
||||||
|
|
||||||
|
DygraphLegend.propTypes = {
|
||||||
|
x: number,
|
||||||
|
xHTML: string,
|
||||||
|
series: arrayOf(
|
||||||
|
shape({
|
||||||
|
color: string,
|
||||||
|
dashHTML: string,
|
||||||
|
isVisible: bool,
|
||||||
|
label: string,
|
||||||
|
y: number,
|
||||||
|
yHTML: string,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
dygraph: shape(),
|
||||||
|
onSnip: func.isRequired,
|
||||||
|
onHide: func.isRequired,
|
||||||
|
onSort: func.isRequired,
|
||||||
|
onInputChange: func.isRequired,
|
||||||
|
onToggleFilter: func.isRequired,
|
||||||
|
filterText: string.isRequired,
|
||||||
|
isAscending: bool.isRequired,
|
||||||
|
sortType: string.isRequired,
|
||||||
|
isHidden: bool.isRequired,
|
||||||
|
legendRef: func.isRequired,
|
||||||
|
isSnipped: bool.isRequired,
|
||||||
|
isFilterVisible: bool.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DygraphLegend
|
|
@ -0,0 +1,87 @@
|
||||||
|
/* eslint-disable no-magic-numbers */
|
||||||
|
import Dygraphs from 'src/external/dygraph'
|
||||||
|
|
||||||
|
export const LINE_COLORS = [
|
||||||
|
'#00C9FF',
|
||||||
|
'#9394FF',
|
||||||
|
'#4ED8A0',
|
||||||
|
'#ff0054',
|
||||||
|
'#ffcc00',
|
||||||
|
'#33aa99',
|
||||||
|
'#9dfc5d',
|
||||||
|
'#92bcc3',
|
||||||
|
'#ca96fb',
|
||||||
|
'#ff00f0',
|
||||||
|
'#38b94a',
|
||||||
|
'#3844b9',
|
||||||
|
'#a0725b',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const darkenColor = colorStr => {
|
||||||
|
// Defined in dygraph-utils.js
|
||||||
|
const color = Dygraphs.toRGB_(colorStr)
|
||||||
|
color.r = Math.floor((255 + color.r) / 2)
|
||||||
|
color.g = Math.floor((255 + color.g) / 2)
|
||||||
|
color.b = Math.floor((255 + color.b) / 2)
|
||||||
|
return `rgb(${color.r},${color.g},${color.b})`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bar Graph code below is from http://dygraphs.com/tests/plotters.html
|
||||||
|
export const multiColumnBarPlotter = e => {
|
||||||
|
// We need to handle all the series simultaneously.
|
||||||
|
if (e.seriesIndex !== 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const g = e.dygraph
|
||||||
|
const ctx = e.drawingContext
|
||||||
|
const sets = e.allSeriesPoints
|
||||||
|
const yBottom = e.dygraph.toDomYCoord(0)
|
||||||
|
|
||||||
|
// Find the minimum separation between x-values.
|
||||||
|
// This determines the bar width.
|
||||||
|
let minSep = Infinity
|
||||||
|
for (let j = 0; j < sets.length; j++) {
|
||||||
|
const points = sets[j]
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
const sep = points[i].canvasx - points[i - 1].canvasx
|
||||||
|
if (sep < minSep) {
|
||||||
|
minSep = sep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const barWidth = Math.floor(2.0 / 3 * minSep)
|
||||||
|
|
||||||
|
const fillColors = []
|
||||||
|
const strokeColors = g.getColors()
|
||||||
|
for (let i = 0; i < strokeColors.length; i++) {
|
||||||
|
fillColors.push(darkenColor(strokeColors[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = 0; j < sets.length; j++) {
|
||||||
|
ctx.fillStyle = fillColors[j]
|
||||||
|
ctx.strokeStyle = strokeColors[j]
|
||||||
|
for (let i = 0; i < sets[j].length; i++) {
|
||||||
|
const p = sets[j][i]
|
||||||
|
const centerX = p.canvasx
|
||||||
|
const xLeft = sets.length === 1
|
||||||
|
? centerX - barWidth / 2
|
||||||
|
: centerX - barWidth / 2 * (1 - j / (sets.length - 1))
|
||||||
|
|
||||||
|
ctx.fillRect(
|
||||||
|
xLeft,
|
||||||
|
p.canvasy,
|
||||||
|
barWidth / sets.length,
|
||||||
|
yBottom - p.canvasy
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx.strokeRect(
|
||||||
|
xLeft,
|
||||||
|
p.canvasy,
|
||||||
|
barWidth / sets.length,
|
||||||
|
yBottom - p.canvasy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.graph-vertical-marker {
|
.graph-vertical-marker {
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
@ -21,69 +20,6 @@
|
||||||
background: linear-gradient(to bottom, fade-out($g20-white, 1) 0%,fade-out($g20-white, 0.71) 6%,fade-out($g20-white, 0.71) 80%,fade-out($g20-white, 1) 100%);
|
background: linear-gradient(to bottom, fade-out($g20-white, 1) 0%,fade-out($g20-white, 0.71) 6%,fade-out($g20-white, 0.71) 80%,fade-out($g20-white, 1) 100%);
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='fade-out($g20-white, 0.71)', endColorstr='fade-out($g20-white, 0.71)',GradientType=0 );
|
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='fade-out($g20-white, 0.71)', endColorstr='fade-out($g20-white, 0.71)',GradientType=0 );
|
||||||
}
|
}
|
||||||
.container--dygraph-legend {
|
|
||||||
transform: translateX(-50%);
|
|
||||||
background-color: $g0-obsidian;
|
|
||||||
display: block !important;
|
|
||||||
position: absolute;
|
|
||||||
padding: 11px;
|
|
||||||
z-index: 500;
|
|
||||||
font-size: 13px;
|
|
||||||
color: $g12-forge;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 13px;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Only animate position that's controlled during rendering.
|
|
||||||
* See http://stackoverflow.com/a/17117992
|
|
||||||
*/
|
|
||||||
// transition: all 0.1s ease;
|
|
||||||
// transition-property: top, right, bottom, left;
|
|
||||||
|
|
||||||
/* Row */
|
|
||||||
/* Styles for Key go here, get overrided by > b */
|
|
||||||
> span {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
opacity: 0.5;
|
|
||||||
padding-top: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 13px;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
color: $g19-ghost;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
/* Border on top of first row */
|
|
||||||
&:first-child {
|
|
||||||
border-top: 2px solid $g4-onyx;
|
|
||||||
padding-top: 6px;
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Legend Key */
|
|
||||||
> b {
|
|
||||||
font-weight: 600 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlight {
|
|
||||||
font-weight: 600;
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
> b {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Axis Labels */
|
/* Axis Labels */
|
||||||
.dygraph-axis-label {
|
.dygraph-axis-label {
|
||||||
|
@ -136,7 +72,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Single Stat Cells */
|
||||||
.single-stat {
|
.single-stat {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -187,3 +123,135 @@
|
||||||
.single-stat--small .single-stat--shadow:after {
|
.single-stat--small .single-stat--shadow:after {
|
||||||
box-shadow: fade-out($g2-kevlar, 0.3) 0 0 30px 10px;
|
box-shadow: fade-out($g2-kevlar, 0.3) 0 0 30px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Legend Styles
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
.dygraph-child-container .dygraph-legend {
|
||||||
|
display: none !important; // hide default legend
|
||||||
|
}
|
||||||
|
.dygraph-legend {
|
||||||
|
background-color: $g0-obsidian;
|
||||||
|
display: block !important;
|
||||||
|
position: absolute;
|
||||||
|
padding: 11px;
|
||||||
|
z-index: 500;
|
||||||
|
border-radius: 3px;
|
||||||
|
min-width: 350px;
|
||||||
|
user-select: text;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
box-shadow: 0 0 10px 2px $g2-kevlar;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dygraph-legend--header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
|
> .btn { margin-left: 4px; }
|
||||||
|
}
|
||||||
|
.dygraph-legend--timestamp {
|
||||||
|
margin-right: 8px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $g13-mist;
|
||||||
|
flex: 1 0 0;
|
||||||
|
}
|
||||||
|
.dygraph-legend--filter {
|
||||||
|
flex: 1 0 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.dygraph-legend--divider {
|
||||||
|
width: 100%;
|
||||||
|
margin: 8px 0;
|
||||||
|
height: 2px;
|
||||||
|
background-color: $g5-pepper;
|
||||||
|
}
|
||||||
|
.dygraph-legend--contents {
|
||||||
|
font-size: 13px;
|
||||||
|
color: $g15-platinum;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 13px;
|
||||||
|
max-height: 123px;
|
||||||
|
overflow-y: auto;
|
||||||
|
@include custom-scrollbar-round($g0-obsidian,$g3-castle);
|
||||||
|
}
|
||||||
|
.dygraph-legend--row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 13px;
|
||||||
|
padding: 3px 0;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
figure {
|
||||||
|
padding-left: 10px;
|
||||||
|
font-family: $code-font;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.highlight {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: $g3-castle;
|
||||||
|
figure {color: $g20-white;}
|
||||||
|
}
|
||||||
|
&.highlight:only-child {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sorting Buttons */
|
||||||
|
.sort-btn {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.sort-btn--arrow {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
height: calc(100% - 16px);
|
||||||
|
width: 2px;
|
||||||
|
background-color: $g20-white;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) scaleX(0.7);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 6px;
|
||||||
|
border-color: transparent;
|
||||||
|
border-bottom-color: $g20-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sort-btn--asc .sort-btn--arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
.sort-btn--top,
|
||||||
|
.sort-btn--bottom {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: $g20-white;
|
||||||
|
left: 6px;
|
||||||
|
}
|
||||||
|
.sort-btn--top {
|
||||||
|
top: -5px;
|
||||||
|
}
|
||||||
|
.sort-btn--bottom {
|
||||||
|
bottom: -6px;
|
||||||
|
}
|
||||||
|
|
|
@ -246,6 +246,7 @@ $rule-builder--radius-lg: 5px;
|
||||||
left: (($rule-builder--dot / 2) - $rule-builder--left-gutter);
|
left: (($rule-builder--dot / 2) - $rule-builder--left-gutter);
|
||||||
}
|
}
|
||||||
.container--dygraph-legend {
|
.container--dygraph-legend {
|
||||||
|
transform: translateX(-50%);
|
||||||
background-color: $g5-pepper;
|
background-color: $g5-pepper;
|
||||||
|
|
||||||
> span:first-child {
|
> span:first-child {
|
||||||
|
|
Loading…
Reference in New Issue