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  }