github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/cluster/components/linegraph/index.tsx (about) 1 // Copyright 2018 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 import d3 from "d3"; 12 import React from "react"; 13 import moment from "moment"; 14 import * as nvd3 from "nvd3"; 15 import { createSelector } from "reselect"; 16 17 import * as protos from "src/js/protos"; 18 import { HoverState, hoverOn, hoverOff } from "src/redux/hover"; 19 import { findChildrenOfType } from "src/util/find"; 20 import { 21 ConfigureLineChart, InitLineChart, CHART_MARGINS, ConfigureLinkedGuideline, 22 } from "src/views/cluster/util/graphs"; 23 import { 24 Metric, MetricProps, Axis, AxisProps, QueryTimeInfo, 25 } from "src/views/shared/components/metricQuery"; 26 import { MetricsDataComponentProps } from "src/views/shared/components/metricQuery"; 27 import Visualization from "src/views/cluster/components/visualization"; 28 import { NanoToMilli } from "src/util/convert"; 29 30 type TSResponse = protos.cockroach.ts.tspb.TimeSeriesQueryResponse; 31 32 interface LineGraphProps extends MetricsDataComponentProps { 33 title?: string; 34 subtitle?: string; 35 legend?: boolean; 36 xAxis?: boolean; 37 tooltip?: React.ReactNode; 38 hoverOn?: typeof hoverOn; 39 hoverOff?: typeof hoverOff; 40 hoverState?: HoverState; 41 } 42 43 interface LineGraphState { 44 lastData?: TSResponse; 45 lastTimeInfo?: QueryTimeInfo; 46 } 47 48 /** 49 * LineGraph displays queried metrics in a line graph. It currently only 50 * supports a single Y-axis, but multiple metrics can be graphed on the same 51 * axis. 52 */ 53 export class LineGraph extends React.Component<LineGraphProps, LineGraphState> { 54 // The SVG Element reference in the DOM used to render the graph. 55 graphEl: React.RefObject<SVGSVGElement> = React.createRef(); 56 57 // A configured NVD3 chart used to render the chart. 58 chart: nvd3.LineChart; 59 60 axis = createSelector( 61 (props: {children?: React.ReactNode}) => props.children, 62 (children) => { 63 const axes: React.ReactElement<AxisProps>[] = findChildrenOfType(children as any, Axis); 64 if (axes.length === 0) { 65 console.warn("LineGraph requires the specification of at least one axis."); 66 return null; 67 } 68 if (axes.length > 1) { 69 console.warn("LineGraph currently only supports a single axis; ignoring additional axes."); 70 } 71 return axes[0]; 72 }); 73 74 metrics = createSelector( 75 (props: {children?: React.ReactNode}) => props.children, 76 (children) => { 77 return findChildrenOfType(children as any, Metric) as React.ReactElement<MetricProps>[]; 78 }); 79 80 initChart() { 81 const axis = this.axis(this.props); 82 if (!axis) { 83 // TODO: Figure out this error condition. 84 return; 85 } 86 87 this.chart = nvd3.models.lineChart(); 88 InitLineChart(this.chart); 89 90 if (axis.props.range) { 91 this.chart.forceY(axis.props.range); 92 } 93 } 94 95 mouseMove = (e: any) => { 96 // TODO(couchand): handle the following cases: 97 // - first series is missing data points 98 // - series are missing data points at different timestamps 99 const datapoints = this.props.data.results[0].datapoints; 100 const timeScale = this.chart.xAxis.scale(); 101 102 // To get the x-coordinate within the chart we subtract the left side of the SVG 103 // element and the left side margin. 104 const x = e.clientX - this.graphEl.current.getBoundingClientRect().left - CHART_MARGINS.left; 105 // Find the time value of the coordinate by asking the scale to invert the value. 106 const t = Math.floor(timeScale.invert(x)); 107 108 // Find which data point is closest to the x-coordinate. 109 let result: moment.Moment; 110 if (datapoints.length) { 111 const series: any = datapoints.map((d: any) => NanoToMilli(d.timestamp_nanos.toNumber())); 112 113 const right = d3.bisectRight(series, t); 114 const left = right - 1; 115 116 let index = 0; 117 118 if (right >= series.length) { 119 // We're hovering over the rightmost point. 120 index = left; 121 } else if (left < 0) { 122 // We're hovering over the leftmost point. 123 index = right; 124 } else { 125 // The general case: we're hovering somewhere over the middle. 126 const leftDistance = t - series[left]; 127 const rightDistance = series[right] - t; 128 129 index = leftDistance < rightDistance ? left : right; 130 } 131 132 result = moment(new Date(series[index])); 133 } 134 135 if (!this.props.hoverState || !result) { 136 return; 137 } 138 139 // Only dispatch if we have something to change to avoid action spamming. 140 if (this.props.hoverState.hoverChart !== this.props.title || !result.isSame(this.props.hoverState.hoverTime)) { 141 this.props.hoverOn({ 142 hoverChart: this.props.title, 143 hoverTime: result, 144 }); 145 } 146 } 147 148 mouseLeave = () => { 149 this.props.hoverOff(); 150 } 151 152 drawChart = () => { 153 // If the document is not visible (e.g. if the window is minimized) we don't 154 // attempt to redraw the chart. Redrawing the chart uses 155 // requestAnimationFrame, which isn't called when the tab is in the 156 // background, and is then apparently queued up and called en masse when the 157 // tab re-enters the foreground. This check prevents the issue in #8896 158 // where switching to a tab with the graphs page open that had been in the 159 // background caused the UI to run out of memory and either lag or crash. 160 // NOTE: This might not work on Android: 161 // http://caniuse.com/#feat=pagevisibility 162 if (!document.hidden) { 163 const metrics = this.metrics(this.props); 164 const axis = this.axis(this.props); 165 if (!axis) { 166 return; 167 } 168 169 ConfigureLineChart( 170 this.chart, this.graphEl.current, metrics, axis, this.props.data, this.props.timeInfo, 171 ); 172 } 173 } 174 175 drawLine = () => { 176 if (!document.hidden) { 177 let hoverTime: moment.Moment; 178 if (this.props.hoverState) { 179 const { currentlyHovering, hoverChart } = this.props.hoverState; 180 // Don't draw the linked guideline on the hovered chart, NVD3 does that for us. 181 if (currentlyHovering && hoverChart !== this.props.title) { 182 hoverTime = this.props.hoverState.hoverTime; 183 } 184 } 185 186 const axis = this.axis(this.props); 187 ConfigureLinkedGuideline(this.chart, this.graphEl.current, axis, this.props.data, hoverTime); 188 } 189 } 190 191 constructor(props: any) { 192 super(props); 193 this.state = { 194 lastData: null, 195 lastTimeInfo: null, 196 }; 197 } 198 199 componentDidMount() { 200 this.initChart(); 201 this.drawChart(); 202 this.drawLine(); 203 // NOTE: This might not work on Android: 204 // http://caniuse.com/#feat=pagevisibility 205 // TODO (maxlang): Check if this element is visible based on scroll state. 206 document.addEventListener("visibilitychange", this.drawChart); 207 } 208 209 componentWillUnmount() { 210 document.removeEventListener("visibilitychange", this.drawChart); 211 } 212 213 componentDidUpdate() { 214 if (this.props.data !== this.state.lastData || this.props.timeInfo !== this.state.lastTimeInfo) { 215 this.drawChart(); 216 this.setState({ 217 lastData: this.props.data, 218 lastTimeInfo: this.props.timeInfo, 219 }); 220 } 221 this.drawLine(); 222 } 223 224 render() { 225 const { title, subtitle, tooltip, data } = this.props; 226 227 let hoverProps: Partial<React.SVGProps<SVGSVGElement>> = {}; 228 if (this.props.hoverOn) { 229 hoverProps = { 230 onMouseMove: this.mouseMove, 231 onMouseLeave: this.mouseLeave, 232 }; 233 } 234 235 return ( 236 <Visualization title={title} subtitle={subtitle} tooltip={tooltip} loading={!data} > 237 <div className="linegraph"> 238 <svg className="graph linked-guideline" ref={this.graphEl} {...hoverProps} /> 239 </div> 240 </Visualization> 241 ); 242 } 243 }