Convert DygraphLegend to typescript

pull/10616/head
Iris Scholten 2018-04-20 15:36:03 -07:00
parent f6d8a9ea41
commit 2b5857b994
4 changed files with 645 additions and 158 deletions

View File

@ -1,17 +1,55 @@
import React, {Component} from 'react' import React, {PureComponent} from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import PropTypes from 'prop-types' import Dygraph from 'dygraphs'
import _ from 'lodash' import _ from 'lodash'
import classnames from 'classnames' import classnames from 'classnames'
import uuid from 'uuid' import uuid from 'uuid'
import * as actions from 'src/dashboards/actions'
import {makeLegendStyles, removeMeasurement} from 'shared/graphs/helpers' import * as actions from 'src/dashboards/actions'
import {SeriesLegendData} from 'src/types/dygraphs'
import DygraphLegendSort from 'src/shared/components/DygraphLegendSort'
import {makeLegendStyles, removeMeasurement} from 'src/shared/graphs/helpers'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {NO_CELL} from 'src/shared/constants' import {NO_CELL} from 'src/shared/constants'
interface ExtendedDygraph extends Dygraph {
graphDiv: HTMLElement
}
interface Props {
dygraph: ExtendedDygraph
cellID: string
onHide: () => void
onShow: (MouseEvent) => void
isDragging: boolean
activeCellID: string
setActiveCell: (cellID: string) => void
}
interface LegendData {
x: string | null
series: SeriesLegendData[]
xHTML: string
}
interface State {
legend: LegendData
sortType: string
isAscending: boolean
filterText: string
isSnipped: boolean
isFilterVisible: boolean
legendStyles: object
pageX: number | null
cellID: string
}
@ErrorHandling @ErrorHandling
class DygraphLegend extends Component { class DygraphLegend extends PureComponent<Props, State> {
private legendRef: HTMLElement | null = null
constructor(props) { constructor(props) {
super(props) super(props)
@ -25,9 +63,10 @@ class DygraphLegend extends Component {
legend: { legend: {
x: null, x: null,
series: [], series: [],
xHTML: '',
}, },
sortType: '', sortType: 'numeric',
isAscending: true, isAscending: false,
filterText: '', filterText: '',
isSnipped: false, isSnipped: false,
isFilterVisible: false, isFilterVisible: false,
@ -37,7 +76,7 @@ class DygraphLegend extends Component {
} }
} }
componentWillUnmount() { public componentWillUnmount() {
if ( if (
!this.props.dygraph.graphDiv || !this.props.dygraph.graphDiv ||
!this.props.dygraph.visibility().find(bool => bool === true) !this.props.dygraph.visibility().find(bool => bool === true)
@ -46,155 +85,38 @@ class DygraphLegend extends Component {
} }
} }
highlightCallback = _.throttle(e => { public render() {
this.props.setActiveCell(this.props.cellID)
this.setState({pageX: e.pageX})
this.props.onShow(e)
}, 60)
legendFormatter = legend => {
if (!legend.x) {
return ''
}
const {legend: prevLegend} = this.state
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.legend = this.setState({legend})
return ''
}
unhighlightCallback = e => {
const { const {
top,
bottom,
left,
right,
} = this.legendNodeRef.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.handleHide()
}
}
handleHide = () => {
this.props.onHide()
this.props.setActiveCell(NO_CELL)
}
handleToggleFilter = () => {
this.setState({
isFilterVisible: !this.state.isFilterVisible,
filterText: '',
})
}
handleSnipLabel = () => {
this.setState({isSnipped: !this.state.isSnipped})
}
handleLegendInputChange = e => {
const {dygraph} = this.props
const {legend} = this.state
const filterText = e.target.value
legend.series.map((s, i) => {
if (!legend.series[i]) {
return dygraph.setVisibility(i, true)
}
dygraph.setVisibility(i, !!legend.series[i].label.match(filterText))
})
this.setState({filterText})
}
handleSortLegend = sortType => () => {
this.setState({sortType, isAscending: !this.state.isAscending})
}
render() {
const {dygraph} = this.props
const {
pageX,
legend, legend,
filterText, filterText,
isSnipped, isSnipped,
sortType,
isAscending, isAscending,
isFilterVisible, isFilterVisible,
} = this.state } = this.state
const withValues = legend.series.filter(s => !_.isNil(s.y))
const sorted = _.sortBy(
withValues,
({y, label}) => (sortType === 'numeric' ? y : label)
)
const ordered = isAscending ? sorted : sorted.reverse()
const filtered = ordered.filter(s => s.label.match(filterText))
const style = makeLegendStyles(dygraph.graphDiv, this.legendNodeRef, pageX)
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={this.handleSortLegend('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={this.handleSortLegend('numeric')}
>
<div className="sort-btn--arrow" />
<div className="sort-btn--top">0</div>
<div className="sort-btn--bottom">9</div>
</button>
)
return ( return (
<div <div
className={`dygraph-legend ${this.hidden}`} className={`dygraph-legend ${this.hidden}`}
ref={el => (this.legendNodeRef = el)} ref={el => (this.legendRef = el)}
onMouseLeave={this.handleHide} onMouseLeave={this.handleHide}
style={style} style={this.styles}
> >
<div className="dygraph-legend--header"> <div className="dygraph-legend--header">
<div className="dygraph-legend--timestamp">{legend.xHTML}</div> <div className="dygraph-legend--timestamp">{legend.xHTML}</div>
{renderSortAlpha} <DygraphLegendSort
{renderSortNum} isAscending={isAscending}
isActive={this.isAphaSort}
top="A"
bottom="Z"
onSort={this.handleSortLegend('alphabetic')}
/>
<DygraphLegendSort
isAscending={isAscending}
isActive={this.isNumSort}
top="0"
bottom="9"
onSort={this.handleSortLegend('numeric')}
/>
<button <button
className={classnames('btn btn-square btn-sm', { className={classnames('btn btn-square btn-sm', {
'btn-default': !isFilterVisible, 'btn-default': !isFilterVisible,
@ -214,7 +136,7 @@ class DygraphLegend extends Component {
Snip Snip
</button> </button>
</div> </div>
{isFilterVisible ? ( {isFilterVisible && (
<input <input
className="dygraph-legend--filter form-control input-sm" className="dygraph-legend--filter form-control input-sm"
type="text" type="text"
@ -223,10 +145,10 @@ class DygraphLegend extends Component {
placeholder="Filter items..." placeholder="Filter items..."
autoFocus={true} autoFocus={true}
/> />
) : null} )}
<div className="dygraph-legend--divider" /> <div className="dygraph-legend--divider" />
<div className="dygraph-legend--contents"> <div className="dygraph-legend--contents">
{filtered.map(({label, color, yHTML, isHighlighted}) => { {this.filtered.map(({label, color, yHTML, isHighlighted}) => {
const seriesClass = isHighlighted const seriesClass = isHighlighted
? 'dygraph-legend--row highlight' ? 'dygraph-legend--row highlight'
: 'dygraph-legend--row' : 'dygraph-legend--row'
@ -244,31 +166,123 @@ class DygraphLegend extends Component {
) )
} }
get isVisible() { private handleHide = () => {
this.props.onHide()
this.props.setActiveCell(NO_CELL)
}
private handleToggleFilter = () => {
this.setState({
isFilterVisible: !this.state.isFilterVisible,
filterText: '',
})
}
private handleSnipLabel = () => {
this.setState({isSnipped: !this.state.isSnipped})
}
private handleLegendInputChange = e => {
const {dygraph} = this.props
const {legend} = this.state
const filterText = e.target.value
legend.series.map((__, i) => {
if (!legend.series[i]) {
return dygraph.setVisibility(i, true)
}
dygraph.setVisibility(i, !!legend.series[i].label.match(filterText))
})
this.setState({filterText})
}
private handleSortLegend = sortType => () => {
this.setState({sortType, isAscending: !this.state.isAscending})
}
private highlightCallback = e => {
this.props.setActiveCell(this.props.cellID)
this.setState({pageX: e.pageX})
this.props.onShow(e)
}
private legendFormatter = legend => {
if (!legend.x) {
return ''
}
const {legend: prevLegend} = this.state
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 ''
}
private 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.handleHide()
}
}
private get filtered() {
const {legend, sortType, isAscending, filterText} = this.state
const withValues = legend.series.filter(s => !_.isNil(s.y))
const sorted = _.sortBy(
withValues,
({y, label}) => (sortType === 'numeric' ? y : label)
)
const ordered = isAscending ? sorted : sorted.reverse()
return ordered.filter(s => s.label.match(filterText))
}
private get isAphaSort(): boolean {
return this.state.sortType === 'alphabetic'
}
private get isNumSort(): boolean {
return this.state.sortType === 'numeric'
}
private get isVisible() {
const {cellID, activeCellID, isDragging} = this.props const {cellID, activeCellID, isDragging} = this.props
return cellID === activeCellID && !isDragging return cellID === activeCellID && !isDragging
} }
get hidden() { private get hidden() {
if (this.isVisible) { if (this.isVisible) {
return '' return ''
} }
return 'hidden' return 'hidden'
} }
}
const {bool, func, shape, string} = PropTypes private get styles() {
const {dygraph: {graphDiv}} = this.props
DygraphLegend.propTypes = { const {pageX} = this.state
dygraph: shape({}), return makeLegendStyles(graphDiv, this.legendRef, pageX)
cellID: string.isRequired, }
onHide: func.isRequired,
onShow: func.isRequired,
isDragging: bool.isRequired,
activeCellID: string.isRequired,
setActiveCell: func.isRequired,
} }
const mapDispatchToProps = { const mapDispatchToProps = {

View File

@ -0,0 +1,33 @@
import React, {PureComponent} from 'react'
import classnames from 'classnames'
interface Props {
isActive: boolean
isAscending: boolean
top: string
bottom: string
onSort: () => void
}
class DygraphLegendSort extends PureComponent<Props> {
public render() {
const {isAscending, top, bottom, onSort, isActive} = this.props
return (
<div
className={classnames('sort-btn btn btn-sm btn-square', {
'btn-primary': isActive,
'btn-default': !isActive,
'sort-btn--asc': isAscending && isActive,
'sort-btn--desc': !isAscending && isActive,
})}
onClick={onSort}
>
<div className="sort-btn--arrow" />
<div className="sort-btn--top">{top}</div>
<div className="sort-btn--bottom">{bottom}</div>
</div>
)
}
}
export default DygraphLegendSort

View File

@ -7,6 +7,8 @@ import {
CELL_TYPE_BAR, CELL_TYPE_BAR,
} from 'src/dashboards/graphics/graph' } from 'src/dashboards/graphics/graph'
export const NO_CELL = 'none'
export const PERMISSIONS = { export const PERMISSIONS = {
ViewAdmin: { ViewAdmin: {
description: 'Can view or edit admin screens', description: 'Can view or edit admin screens',

438
ui/src/types/dygraphs.ts Normal file
View File

@ -0,0 +1,438 @@
export type DataArray = number[][]
export type Data = string | DataArray | google.visualization.DataTable
export interface PerSeriesOptions {
/**
* Set to either 'y1' or 'y2' to assign a series to a y-axis (primary or secondary). Must be
* set per-series.
*/
axis?: 'y1' | 'y2'
/**
* A per-series color definition. Used in conjunction with, and overrides, the colors option.
*/
color?: string
/**
* Draw a small dot at each point, in addition to a line going through the point. This makes
* the individual data points easier to see, but can increase visual clutter in the chart.
* The small dot can be replaced with a custom rendering by supplying a <a
* href='#drawPointCallback'>drawPointCallback</a>.
*/
drawPoints?: boolean
/**
* Error bars (or custom bars) for each series are drawn in the same color as the series, but
* with partial transparency. This sets the transparency. A value of 0.0 means that the error
* bars will not be drawn, whereas a value of 1.0 means that the error bars will be as dark
* as the line for the series itself. This can be used to produce chart lines whose thickness
* varies at each point.
*/
fillAlpha?: number
/**
* Should the area underneath the graph be filled? This option is not compatible with error
* bars. This may be set on a <a href='per-axis.html'>per-series</a> basis.
*/
fillGraph?: boolean
/**
* The size in pixels of the dot drawn over highlighted points.
*/
highlightCircleSize?: number
/**
* The size of the dot to draw on each point in pixels (see drawPoints). A dot is always
* drawn when a point is "isolated", i.e. there is a missing point on either side of it. This
* also controls the size of those dots.
*/
pointSize?: number
/**
* Mark this series for inclusion in the range selector. The mini plot curve will be an
* average of all such series. If this is not specified for any series, the default behavior
* is to average all the series. Setting it for one series will result in that series being
* charted alone in the range selector.
*/
showInRangeSelector?: boolean
/**
* When set, display the graph as a step plot instead of a line plot. This option may either
* be set for the whole graph or for single series.
*/
stepPlot?: boolean
/**
* Draw a border around graph lines to make crossing lines more easily distinguishable.
* Useful for graphs with many lines.
*/
strokeBorderWidth?: number
/**
* Color for the line border used if strokeBorderWidth is set.
*/
strokeBorderColor?: string
/**
* A custom pattern array where the even index is a draw and odd is a space in pixels. If
* null then it draws a solid line. The array should have a even length as any odd lengthed
* array could be expressed as a smaller even length array. This is used to create dashed
* lines.
*/
strokePattern?: number[]
/**
* The width of the lines connecting data points. This can be used to increase the contrast
* or some graphs.
*/
strokeWidth?: number
}
export interface PerAxisOptions {
/**
* Color for x- and y-axis labels. This is a CSS color string.
*/
axisLabelColor?: string
/**
* Size of the font (in pixels) to use in the axis labels, both x- and y-axis.
*/
axisLabelFontSize?: number
/**
* Function to call to format the tick values that appear along an axis. This is usually set
* on a <a href='per-axis.html'>per-axis</a> basis.
*/
axisLabelFormatter?: (
v: number | Date,
granularity: number,
opts: (name: string) => any,
dygraph: Dygraph
) => any
/**
* Width (in pixels) of the containing divs for x- and y-axis labels. For the y-axis, this
* also controls the width of the y-axis. Note that for the x-axis, this is independent from
* pixelsPerLabel, which controls the spacing between labels.
*/
axisLabelWidth?: number
/**
* Color of the x- and y-axis lines. Accepts any value which the HTML canvas strokeStyle
* attribute understands, e.g. 'black' or 'rgb(0, 100, 255)'.
*/
axisLineColor?: string
/**
* Thickness (in pixels) of the x- and y-axis lines.
*/
axisLineWidth?: number
/**
* The size of the line to display next to each tick mark on x- or y-axes.
*/
axisTickSize?: number
/**
* Whether to draw the specified axis. This may be set on a per-axis basis to define the
* visibility of each axis separately. Setting this to false also prevents axis ticks from
* being drawn and reclaims the space for the chart grid/lines.
*/
drawAxis?: boolean
/**
* The color of the gridlines. This may be set on a per-axis basis to define each axis' grid
* separately.
*/
gridLineColor?: string
/**
* A custom pattern array where the even index is a draw and odd is a space in pixels. If
* null then it draws a solid line. The array should have a even length as any odd lengthed
* array could be expressed as a smaller even length array. This is used to create dashed
* gridlines.
*/
gridLinePattern?: number[]
/**
* Thickness (in pixels) of the gridlines drawn under the chart. The vertical/horizontal
* gridlines can be turned off entirely by using the drawXGrid and drawYGrid options. This
* may be set on a per-axis basis to define each axis' grid separately.
*/
gridLineWidth?: number
/**
* Only valid for y and y2, has no effect on x: This option defines whether the y axes should
* align their ticks or if they should be independent. Possible combinations: 1.) y=true,
* y2=false (default): y is the primary axis and the y2 ticks are aligned to the the ones of
* y. (only 1 grid) 2.) y=false, y2=true: y2 is the primary axis and the y ticks are aligned
* to the the ones of y2. (only 1 grid) 3.) y=true, y2=true: Both axis are independent and
* have their own ticks. (2 grids) 4.) y=false, y2=false: Invalid configuration causes an
* error.
*/
independentTicks?: boolean
/**
* When set for the y-axis or x-axis, the graph shows that axis in log scale. Any values less
* than or equal to zero are not displayed. Showing log scale with ranges that go below zero
* will result in an unviewable graph.
*
* Not compatible with showZero. connectSeparatedPoints is ignored. This is ignored for
* date-based x-axes.
*/
logscale?: boolean
/**
* When displaying numbers in normal (not scientific) mode, large numbers will be displayed
* with many trailing zeros (e.g. 100000000 instead of 1e9). This can lead to unwieldy y-axis
* labels. If there are more than <code>maxNumberWidth</code> digits to the left of the
* decimal in a number, dygraphs will switch to scientific notation, even when not operating
* in scientific mode. If you'd like to see all those digits, set this to something large,
* like 20 or 30.
*/
maxNumberWidth?: number
/**
* Number of pixels to require between each x- and y-label. Larger values will yield a
* sparser axis with fewer ticks. This is set on a <a href='per-axis.html'>per-axis</a>
* basis.
*/
pixelsPerLabel?: number
/**
* By default, dygraphs displays numbers with a fixed number of digits after the decimal
* point. If you'd prefer to have a fixed number of significant figures, set this option to
* that number of sig figs. A value of 2, for instance, would cause 1 to be display as 1.0
* and 1234 to be displayed as 1.23e+3.
*/
sigFigs?: number
/**
* This lets you specify an arbitrary function to generate tick marks on an axis. The tick
* marks are an array of (value, label) pairs. The built-in functions go to great lengths to
* choose good tick marks so, if you set this option, you'll most likely want to call one of
* them and modify the result. See dygraph-tickers.js for an extensive discussion. This is
* set on a <a href='per-axis.html'>per-axis</a> basis.
*/
ticker?: (
min: number,
max: number,
pixels: number,
opts: (name: string) => any,
dygraph: Dygraph,
vals: number[]
) => Array<{v: number; label: string}>
/**
* Function to provide a custom display format for the values displayed on mouseover. This
* does not affect the values that appear on tick marks next to the axes. To format those,
* see axisLabelFormatter. This is usually set on a <a href='per-axis.html'>per-axis</a>
* basis.
*/
valueFormatter?: (
v: number,
opts: (name: string) => any,
seriesName: string,
dygraph: Dygraph,
row: number,
col: number
) => any
/**
* Explicitly set the vertical range of the graph to [low, high]. This may be set on a
* per-axis basis to define each y-axis separately. If either limit is unspecified, it will
* be calculated automatically (e.g. [null, 30] to automatically calculate just the lower
* bound)
*/
valueRange?: number[]
/**
* Whether to display gridlines in the chart. This may be set on a per-axis basis to define
* the visibility of each axis' grid separately.
*/
drawGrid?: boolean
/**
* Show K/M/B for thousands/millions/billions on y-axis.
*/
labelsKMB?: boolean
/**
* Show k/M/G for kilo/Mega/Giga on y-axis. This is different than <code>labelsKMB</code> in
* that it uses base 2, not 10.
*/
labelsKMG2?: boolean
}
export interface SeriesLegendData {
/**
* Assigned or generated series color
*/
color: string
/**
* Series line dash
*/
dashHTML: string
/**
* Whether currently focused or not
*/
isHighlighted: boolean
/**
* Whether the series line is inside the selected/zoomed region
*/
isVisible: boolean
/**
* Assigned label to this series
*/
label: string
/**
* Generated label html for this series
*/
labelHTML: string
/**
* y value of this series
*/
y: number
/**
* Generated html for y value
*/
yHTML: string
}
export interface LegendData {
/**
* x value of highlighted points
*/
x: number
/**
* Generated HTML for x value
*/
xHTML: string
/**
* Series data for the highlighted points
*/
series: SeriesLegendData[]
/**
* Dygraph object for this graph
*/
dygraph: Dygraph
}
export interface SeriesProperties {
name: string
column: number
visible: boolean
color: string
axis: number
}
export interface Area {
x: number
y: number
w: number
h: number
}
/**
* Point structure.
*
* xval_* and yval_* are the original unscaled data values,
* while x_* and y_* are scaled to the range (0.0-1.0) for plotting.
* yval_stacked is the cumulative Y value used for stacking graphs,
* and bottom/top/minus/plus are used for error bar graphs.
*/
export interface Point {
idx: number
name: string
x?: number
xval?: number
y_bottom?: number
y?: number
y_stacked?: number
y_top?: number
yval_minus?: number
yval?: number
yval_plus?: number
yval_stacked?: number
annotation?: dygraphs.Annotation
}
export interface Annotation {
/** The name of the series to which the annotated point belongs. */
series: string
/**
* The x value of the point. This should be the same as the value
* you specified in your CSV file, e.g. "2010-09-13".
* You must set either x or xval.
*/
x?: number | string
/**
* numeric value of the point, or millis since epoch.
*/
xval?: number
/** Text that will appear on the annotation's flag. */
shortText?: string
/** A longer description of the annotation which will appear when the user hovers over it. */
text?: string
/**
* Specify in place of shortText to mark the annotation with an image rather than text.
* If you specify this, you must specify width and height.
*/
icon?: string
/** Width (in pixels) of the annotation flag or icon. */
width?: number
/** Height (in pixels) of the annotation flag or icon. */
height?: number
/** CSS class to use for styling the annotation. */
cssClass?: string
/** Height of the tick mark (in pixels) connecting the point to its flag or icon. */
tickHeight?: number
/** If true, attach annotations to the x-axis, rather than to actual points. */
attachAtBottom?: boolean
div?: HTMLDivElement
/** This function is called whenever the user clicks on this annotation. */
clickHandler?: (
annotation: dygraphs.Annotation,
point: Point,
dygraph: Dygraph,
event: MouseEvent
) => any
/** This function is called whenever the user mouses over this annotation. */
mouseOverHandler?: (
annotation: dygraphs.Annotation,
point: Point,
dygraph: Dygraph,
event: MouseEvent
) => any
/** This function is called whenever the user mouses out of this annotation. */
mouseOutHandler?: (
annotation: dygraphs.Annotation,
point: Point,
dygraph: Dygraph,
event: MouseEvent
) => any
/** this function is called whenever the user double-clicks on this annotation. */
dblClickHandler?: (
annotation: dygraphs.Annotation,
point: Point,
dygraph: Dygraph,
event: MouseEvent
) => any
}
export type Axis = 'x' | 'y' | 'y2'