|
@@ -1,280 +0,0 @@
|
|
|
-import $ from 'jquery';
|
|
|
|
|
-import React, { PureComponent } from 'react';
|
|
|
|
|
-import difference from 'lodash/difference';
|
|
|
|
|
-
|
|
|
|
|
-import 'vendor/flot/jquery.flot';
|
|
|
|
|
-import 'vendor/flot/jquery.flot.time';
|
|
|
|
|
-import 'vendor/flot/jquery.flot.selection';
|
|
|
|
|
-import 'vendor/flot/jquery.flot.stack';
|
|
|
|
|
-
|
|
|
|
|
-import { GraphLegend, LegendItem, LegendDisplayMode } from '@grafana/ui';
|
|
|
|
|
-import { TimeZone, AbsoluteTimeRange } from '@grafana/data';
|
|
|
|
|
-import TimeSeries from 'app/core/time_series2';
|
|
|
|
|
-
|
|
|
|
|
-const MAX_NUMBER_OF_TIME_SERIES = 20;
|
|
|
|
|
-
|
|
|
|
|
-// Copied from graph.ts
|
|
|
|
|
-function time_format(ticks: number, min: number, max: number) {
|
|
|
|
|
- if (min && max && ticks) {
|
|
|
|
|
- const range = max - min;
|
|
|
|
|
- const secPerTick = range / ticks / 1000;
|
|
|
|
|
- const oneDay = 86400000;
|
|
|
|
|
- const oneYear = 31536000000;
|
|
|
|
|
-
|
|
|
|
|
- if (secPerTick <= 45) {
|
|
|
|
|
- return '%H:%M:%S';
|
|
|
|
|
- }
|
|
|
|
|
- if (secPerTick <= 7200 || range <= oneDay) {
|
|
|
|
|
- return '%H:%M';
|
|
|
|
|
- }
|
|
|
|
|
- if (secPerTick <= 80000) {
|
|
|
|
|
- return '%m/%d %H:%M';
|
|
|
|
|
- }
|
|
|
|
|
- if (secPerTick <= 2419200 || range <= oneYear) {
|
|
|
|
|
- return '%m/%d';
|
|
|
|
|
- }
|
|
|
|
|
- return '%Y-%m';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return '%H:%M';
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const FLOT_OPTIONS: any = {
|
|
|
|
|
- legend: {
|
|
|
|
|
- show: false,
|
|
|
|
|
- },
|
|
|
|
|
- series: {
|
|
|
|
|
- lines: {
|
|
|
|
|
- linewidth: 1,
|
|
|
|
|
- zero: false,
|
|
|
|
|
- },
|
|
|
|
|
- shadowSize: 0,
|
|
|
|
|
- },
|
|
|
|
|
- grid: {
|
|
|
|
|
- minBorderMargin: 0,
|
|
|
|
|
- markings: [],
|
|
|
|
|
- backgroundColor: null,
|
|
|
|
|
- borderWidth: 0,
|
|
|
|
|
- // hoverable: true,
|
|
|
|
|
- clickable: true,
|
|
|
|
|
- color: '#a1a1a1',
|
|
|
|
|
- margin: { left: 0, right: 0 },
|
|
|
|
|
- labelMarginX: 0,
|
|
|
|
|
- },
|
|
|
|
|
- selection: {
|
|
|
|
|
- mode: 'x',
|
|
|
|
|
- color: '#666',
|
|
|
|
|
- },
|
|
|
|
|
- // crosshair: {
|
|
|
|
|
- // mode: 'x',
|
|
|
|
|
- // },
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-interface GraphProps {
|
|
|
|
|
- data: any[];
|
|
|
|
|
- height?: number;
|
|
|
|
|
- width?: number;
|
|
|
|
|
- id?: string;
|
|
|
|
|
- range: AbsoluteTimeRange;
|
|
|
|
|
- timeZone: TimeZone;
|
|
|
|
|
- split?: boolean;
|
|
|
|
|
- userOptions?: any;
|
|
|
|
|
- onChangeTime?: (range: AbsoluteTimeRange) => void;
|
|
|
|
|
- onToggleSeries?: (alias: string, hiddenSeries: string[]) => void;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-interface GraphState {
|
|
|
|
|
- /**
|
|
|
|
|
- * Type parameter refers to the `alias` property of a `TimeSeries`.
|
|
|
|
|
- * Consequently, all series sharing the same alias will share visibility state.
|
|
|
|
|
- */
|
|
|
|
|
- hiddenSeries: string[];
|
|
|
|
|
- showAllTimeSeries: boolean;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-export class Graph extends PureComponent<GraphProps, GraphState> {
|
|
|
|
|
- $el: any;
|
|
|
|
|
- dynamicOptions: any = null;
|
|
|
|
|
-
|
|
|
|
|
- state: GraphState = {
|
|
|
|
|
- hiddenSeries: [],
|
|
|
|
|
- showAllTimeSeries: false,
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- getGraphData(): TimeSeries[] {
|
|
|
|
|
- const { data } = this.props;
|
|
|
|
|
-
|
|
|
|
|
- return this.state.showAllTimeSeries ? data : data.slice(0, MAX_NUMBER_OF_TIME_SERIES);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- componentDidMount() {
|
|
|
|
|
- this.draw();
|
|
|
|
|
- this.$el = $(`#${this.props.id}`);
|
|
|
|
|
- this.$el.bind('plotselected', this.onPlotSelected);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- componentDidUpdate(prevProps: GraphProps, prevState: GraphState) {
|
|
|
|
|
- if (
|
|
|
|
|
- prevProps.data !== this.props.data ||
|
|
|
|
|
- prevProps.range !== this.props.range ||
|
|
|
|
|
- prevProps.split !== this.props.split ||
|
|
|
|
|
- prevProps.height !== this.props.height ||
|
|
|
|
|
- prevProps.width !== this.props.width ||
|
|
|
|
|
- prevState.hiddenSeries !== this.state.hiddenSeries
|
|
|
|
|
- ) {
|
|
|
|
|
- this.draw();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- componentWillUnmount() {
|
|
|
|
|
- this.$el.unbind('plotselected', this.onPlotSelected);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- onPlotSelected = (event: JQueryEventObject, ranges: { xaxis: { from: number; to: number } }) => {
|
|
|
|
|
- const { onChangeTime } = this.props;
|
|
|
|
|
- if (onChangeTime) {
|
|
|
|
|
- this.props.onChangeTime({
|
|
|
|
|
- from: ranges.xaxis.from,
|
|
|
|
|
- to: ranges.xaxis.to,
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- getDynamicOptions() {
|
|
|
|
|
- const { range, width, timeZone } = this.props;
|
|
|
|
|
- const ticks = (width || 0) / 100;
|
|
|
|
|
- const min = range.from;
|
|
|
|
|
- const max = range.to;
|
|
|
|
|
- return {
|
|
|
|
|
- xaxis: {
|
|
|
|
|
- mode: 'time',
|
|
|
|
|
- min: min,
|
|
|
|
|
- max: max,
|
|
|
|
|
- label: 'Datetime',
|
|
|
|
|
- ticks: ticks,
|
|
|
|
|
- timezone: timeZone,
|
|
|
|
|
- timeformat: time_format(ticks, min, max),
|
|
|
|
|
- },
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- onShowAllTimeSeries = () => {
|
|
|
|
|
- this.setState(
|
|
|
|
|
- {
|
|
|
|
|
- showAllTimeSeries: true,
|
|
|
|
|
- },
|
|
|
|
|
- this.draw
|
|
|
|
|
- );
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- draw() {
|
|
|
|
|
- const { userOptions = {} } = this.props;
|
|
|
|
|
- const { hiddenSeries } = this.state;
|
|
|
|
|
- const data = this.getGraphData();
|
|
|
|
|
-
|
|
|
|
|
- const $el = $(`#${this.props.id}`);
|
|
|
|
|
- let series = [{ data: [[0, 0]] }];
|
|
|
|
|
-
|
|
|
|
|
- if (data && data.length > 0) {
|
|
|
|
|
- series = data
|
|
|
|
|
- .filter((ts: TimeSeries) => hiddenSeries.indexOf(ts.alias) === -1)
|
|
|
|
|
- .map((ts: TimeSeries) => ({
|
|
|
|
|
- color: ts.color,
|
|
|
|
|
- label: ts.label,
|
|
|
|
|
- data: ts.getFlotPairs('null'),
|
|
|
|
|
- }));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- this.dynamicOptions = this.getDynamicOptions();
|
|
|
|
|
-
|
|
|
|
|
- const options = {
|
|
|
|
|
- ...FLOT_OPTIONS,
|
|
|
|
|
- ...this.dynamicOptions,
|
|
|
|
|
- ...userOptions,
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- $.plot($el, series, options);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- getLegendItems = (): LegendItem[] => {
|
|
|
|
|
- const { hiddenSeries } = this.state;
|
|
|
|
|
- const data = this.getGraphData();
|
|
|
|
|
-
|
|
|
|
|
- return data.map(series => {
|
|
|
|
|
- return {
|
|
|
|
|
- label: series.alias,
|
|
|
|
|
- color: series.color,
|
|
|
|
|
- isVisible: hiddenSeries.indexOf(series.alias) === -1,
|
|
|
|
|
- yAxis: 1,
|
|
|
|
|
- };
|
|
|
|
|
- });
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- onSeriesToggle(label: string, event: React.MouseEvent<HTMLElement>) {
|
|
|
|
|
- // This implementation is more or less a copy of GraphPanel's logic.
|
|
|
|
|
- // TODO: we need to use Graph's panel controller or split it into smaller
|
|
|
|
|
- // controllers to remove code duplication. Right now we cant easily use that, since Explore
|
|
|
|
|
- // is not using DataFrame for graph yet
|
|
|
|
|
-
|
|
|
|
|
- const exclusive = event.ctrlKey || event.metaKey || event.shiftKey;
|
|
|
|
|
-
|
|
|
|
|
- this.setState((state, props) => {
|
|
|
|
|
- const { data, onToggleSeries } = props;
|
|
|
|
|
- let nextHiddenSeries: string[] = [];
|
|
|
|
|
- if (exclusive) {
|
|
|
|
|
- // Toggling series with key makes the series itself to toggle
|
|
|
|
|
- if (state.hiddenSeries.indexOf(label) > -1) {
|
|
|
|
|
- nextHiddenSeries = state.hiddenSeries.filter(series => series !== label);
|
|
|
|
|
- } else {
|
|
|
|
|
- nextHiddenSeries = state.hiddenSeries.concat([label]);
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- // Toggling series with out key toggles all the series but the clicked one
|
|
|
|
|
- const allSeriesLabels = data.map(series => series.label);
|
|
|
|
|
-
|
|
|
|
|
- if (state.hiddenSeries.length + 1 === allSeriesLabels.length) {
|
|
|
|
|
- nextHiddenSeries = [];
|
|
|
|
|
- } else {
|
|
|
|
|
- nextHiddenSeries = difference(allSeriesLabels, [label]);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (onToggleSeries) {
|
|
|
|
|
- onToggleSeries(label, nextHiddenSeries);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- hiddenSeries: nextHiddenSeries,
|
|
|
|
|
- };
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- render() {
|
|
|
|
|
- const { height = 100, id = 'graph' } = this.props;
|
|
|
|
|
- return (
|
|
|
|
|
- <>
|
|
|
|
|
- {this.props.data && this.props.data.length > MAX_NUMBER_OF_TIME_SERIES && !this.state.showAllTimeSeries && (
|
|
|
|
|
- <div className="time-series-disclaimer">
|
|
|
|
|
- <i className="fa fa-fw fa-warning disclaimer-icon" />
|
|
|
|
|
- {`Showing only ${MAX_NUMBER_OF_TIME_SERIES} time series. `}
|
|
|
|
|
- <span className="show-all-time-series" onClick={this.onShowAllTimeSeries}>{`Show all ${
|
|
|
|
|
- this.props.data.length
|
|
|
|
|
- }`}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- <div id={id} className="explore-graph" style={{ height }} />
|
|
|
|
|
-
|
|
|
|
|
- <GraphLegend
|
|
|
|
|
- items={this.getLegendItems()}
|
|
|
|
|
- displayMode={LegendDisplayMode.List}
|
|
|
|
|
- placement="under"
|
|
|
|
|
- onLabelClick={(item, event) => {
|
|
|
|
|
- this.onSeriesToggle(item.label, event);
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
- </>
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-export default Graph;
|
|
|