瀏覽代碼

Explore: Replaces TimeSeries with GraphSeriesXY (#18475)

* Wip: Compiles and runs

* WIP: Logs Graph partially working

* Refactor: Adds GraphSeriesToggler

* Refactor: Adds tickDecimals to YAxis

* Refactor: Adds TimeZone and PlotSelection to Graph

* Refactor: Makes the graphResult work in Explore

* Refactor: Adds ExploreGraphPanel that is used by Logs and Explore

* Fix: Fixes strange behaviour with ExploreMode not beeing changed

* Fix: Adds onSelectionChanged to GraphWithLegend

* Refactor: Cleans up unused comments

* ExploreGraph: Disable colorpicker
Hugo Häggmark 6 年之前
父節點
當前提交
4b3440325e
共有 30 個文件被更改,包括 551 次插入998 次删除
  1. 7 1
      packages/grafana-data/src/types/graph.ts
  2. 3 2
      packages/grafana-data/src/types/logs.ts
  3. 21 1
      packages/grafana-data/src/utils/processDataFrame.ts
  4. 45 7
      packages/grafana-ui/src/components/Graph/Graph.tsx
  5. 1 1
      packages/grafana-ui/src/components/Graph/GraphWithLegend.story.tsx
  6. 12 3
      packages/grafana-ui/src/components/Graph/GraphWithLegend.tsx
  7. 11 5
      packages/grafana-ui/src/components/Graph/mockGraphWithLegendData.ts
  8. 2 1
      packages/grafana-ui/src/types/panel.ts
  9. 1 1
      packages/grafana-ui/src/utils/fieldDisplay.ts
  10. 4 6
      packages/grafana-ui/src/utils/flotPairs.test.ts
  11. 3 5
      packages/grafana-ui/src/utils/flotPairs.ts
  12. 36 13
      public/app/core/logs_model.ts
  13. 2 1
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  14. 14 8
      public/app/features/explore/Explore.tsx
  15. 173 0
      public/app/features/explore/ExploreGraphPanel.tsx
  16. 0 50
      public/app/features/explore/Graph.test.tsx
  17. 0 280
      public/app/features/explore/Graph.tsx
  18. 0 100
      public/app/features/explore/GraphContainer.tsx
  19. 8 32
      public/app/features/explore/Logs.tsx
  20. 2 2
      public/app/features/explore/QueryRow.tsx
  21. 0 315
      public/app/features/explore/__snapshots__/Graph.test.tsx.snap
  22. 2 2
      public/app/features/explore/state/actionTypes.ts
  23. 48 77
      public/app/features/explore/utils/ResultProcessor.test.ts
  24. 37 35
      public/app/features/explore/utils/ResultProcessor.ts
  25. 2 0
      public/app/plugins/panel/graph2/GraphPanel.tsx
  26. 23 46
      public/app/plugins/panel/graph2/GraphPanelController.tsx
  27. 85 0
      public/app/plugins/panel/graph2/GraphSeriesToggler.tsx
  28. 4 2
      public/app/plugins/panel/graph2/getGraphSeriesModel.ts
  29. 3 1
      public/app/plugins/panel/graph2/types.ts
  30. 2 1
      public/app/types/explore.ts

+ 7 - 1
packages/grafana-data/src/types/graph.ts

@@ -1,5 +1,11 @@
 import { DisplayValue } from './displayValue';
 
+export interface YAxis {
+  index: number;
+  min?: number;
+  tickDecimals?: number;
+}
+
 export type GraphSeriesValue = number | null;
 
 /** View model projection of a series */
@@ -9,7 +15,7 @@ export interface GraphSeriesXY {
   data: GraphSeriesValue[][]; // [x,y][]
   info?: DisplayValue[]; // Legend info
   isVisible: boolean;
-  yAxis: number;
+  yAxis: YAxis;
 }
 
 export interface CreatePlotOverlay {

+ 3 - 2
packages/grafana-data/src/types/logs.ts

@@ -1,4 +1,5 @@
-import { Labels, TimeSeries } from './data';
+import { Labels } from './data';
+import { GraphSeriesXY } from './graph';
 
 /**
  * Mapping of log level abbreviation to canonical log level.
@@ -54,7 +55,7 @@ export interface LogsModel {
   hasUniqueLabels: boolean;
   meta?: LogsMetaItem[];
   rows: LogRowModel[];
-  series?: TimeSeries[];
+  series?: GraphSeriesXY[];
 }
 
 export interface LogSearchMatch {

+ 21 - 1
packages/grafana-data/src/utils/processDataFrame.ts

@@ -4,7 +4,7 @@ import isString from 'lodash/isString';
 import isBoolean from 'lodash/isBoolean';
 
 // Types
-import { DataFrame, Field, TimeSeries, FieldType, TableData, Column } from '../types/index';
+import { DataFrame, Field, TimeSeries, FieldType, TableData, Column, GraphSeriesXY } from '../types/index';
 import { isDateTime } from './moment_wrapper';
 
 function convertTableToDataFrame(table: TableData): DataFrame {
@@ -44,6 +44,23 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
   };
 }
 
+function convertGraphSeriesToDataFrame(graphSeries: GraphSeriesXY): DataFrame {
+  return {
+    name: graphSeries.label,
+    fields: [
+      {
+        name: graphSeries.label || 'Value',
+      },
+      {
+        name: 'Time',
+        type: FieldType.time,
+        unit: 'dateTimeAsIso',
+      },
+    ],
+    rows: graphSeries.data,
+  };
+}
+
 // PapaParse Dynamic Typing regex:
 // https://github.com/mholt/PapaParse/blob/master/papaparse.js#L998
 const NUMBER = /^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i;
@@ -145,6 +162,9 @@ export const toDataFrame = (data: any): DataFrame => {
   if (data.hasOwnProperty('datapoints')) {
     return convertTimeSeriesToDataFrame(data);
   }
+  if (data.hasOwnProperty('data')) {
+    return convertGraphSeriesToDataFrame(data);
+  }
   if (data.hasOwnProperty('columns')) {
     return convertTableToDataFrame(data);
   }

+ 45 - 7
packages/grafana-ui/src/components/Graph/Graph.tsx

@@ -4,16 +4,20 @@ import React, { PureComponent } from 'react';
 import uniqBy from 'lodash/uniqBy';
 
 // Types
-import { TimeRange, GraphSeriesXY } from '@grafana/data';
+import { TimeRange, GraphSeriesXY, AbsoluteTimeRange, TimeZone, DefaultTimeZone } from '@grafana/data';
 
 export interface GraphProps {
   series: GraphSeriesXY[];
   timeRange: TimeRange; // NOTE: we should aim to make `time` a property of the axis, not force it for all graphs
+  timeZone: TimeZone; // NOTE: we should aim to make `time` a property of the axis, not force it for all graphs
   showLines?: boolean;
   showPoints?: boolean;
   showBars?: boolean;
   width: number;
   height: number;
+  isStacked?: boolean;
+  lineWidth?: number;
+  onSelectionChanged?: (range: AbsoluteTimeRange) => void;
 }
 
 export class Graph extends PureComponent<GraphProps> {
@@ -21,9 +25,12 @@ export class Graph extends PureComponent<GraphProps> {
     showLines: true,
     showPoints: false,
     showBars: false,
+    isStacked: false,
+    lineWidth: 1,
   };
 
   element: HTMLElement | null = null;
+  $element: any;
 
   componentDidUpdate() {
     this.draw();
@@ -31,14 +38,32 @@ export class Graph extends PureComponent<GraphProps> {
 
   componentDidMount() {
     this.draw();
+    if (this.element) {
+      this.$element = $(this.element);
+      this.$element.bind('plotselected', this.onPlotSelected);
+    }
+  }
+
+  componentWillUnmount() {
+    this.$element.unbind('plotselected', this.onPlotSelected);
   }
 
+  onPlotSelected = (event: JQueryEventObject, ranges: { xaxis: { from: number; to: number } }) => {
+    const { onSelectionChanged } = this.props;
+    if (onSelectionChanged) {
+      onSelectionChanged({
+        from: ranges.xaxis.from,
+        to: ranges.xaxis.to,
+      });
+    }
+  };
+
   draw() {
     if (this.element === null) {
       return;
     }
 
-    const { width, series, timeRange, showLines, showBars, showPoints } = this.props;
+    const { width, series, timeRange, showLines, showBars, showPoints, isStacked, lineWidth, timeZone } = this.props;
 
     if (!width) {
       return;
@@ -49,10 +74,16 @@ export class Graph extends PureComponent<GraphProps> {
     const max = timeRange.to.valueOf();
     const yaxes = uniqBy(
       series.map(s => {
+        const index = s.yAxis ? s.yAxis.index : 1;
+        const min = s.yAxis && !isNaN(s.yAxis.min as number) ? s.yAxis.min : null;
+        const tickDecimals = s.yAxis && !isNaN(s.yAxis.tickDecimals as number) ? s.yAxis.tickDecimals : null;
+
         return {
           show: true,
-          index: s.yAxis,
-          position: s.yAxis === 1 ? 'left' : 'right',
+          index,
+          position: index === 1 ? 'left' : 'right',
+          min,
+          tickDecimals,
         };
       }),
       yAxisConfig => yAxisConfig.index
@@ -62,9 +93,10 @@ export class Graph extends PureComponent<GraphProps> {
         show: false,
       },
       series: {
+        stack: isStacked,
         lines: {
           show: showLines,
-          linewidth: 1,
+          linewidth: lineWidth,
           zero: false,
         },
         points: {
@@ -78,7 +110,7 @@ export class Graph extends PureComponent<GraphProps> {
           fill: 1,
           barWidth: 1,
           zero: false,
-          lineWidth: 0,
+          lineWidth: lineWidth,
         },
         shadowSize: 0,
       },
@@ -89,6 +121,7 @@ export class Graph extends PureComponent<GraphProps> {
         label: 'Datetime',
         ticks: ticks,
         timeformat: timeFormat(ticks, min, max),
+        timezone: timeZone ? timeZone : DefaultTimeZone,
       },
       yaxes,
       grid: {
@@ -102,6 +135,10 @@ export class Graph extends PureComponent<GraphProps> {
         margin: { left: 0, right: 0 },
         labelMarginX: 0,
       },
+      selection: {
+        mode: 'x',
+        color: '#666',
+      },
     };
 
     try {
@@ -113,9 +150,10 @@ export class Graph extends PureComponent<GraphProps> {
   }
 
   render() {
+    const { height } = this.props;
     return (
       <div className="graph-panel">
-        <div className="graph-panel__chart" ref={e => (this.element = e)} />
+        <div className="graph-panel__chart" ref={e => (this.element = e)} style={{ height }} />
       </div>
     );
   }

+ 1 - 1
packages/grafana-ui/src/components/Graph/GraphWithLegend.story.tsx

@@ -74,7 +74,7 @@ GraphWithLegendStories.add('default', () => {
         .map(s => s.trim())
         .indexOf(s.label.split('-')[0]) > -1
     ) {
-      s.yAxis = 2;
+      s.yAxis = { index: 2 };
     }
 
     return s;

+ 12 - 3
packages/grafana-ui/src/components/Graph/GraphWithLegend.tsx

@@ -2,7 +2,7 @@
 import _ from 'lodash';
 import React from 'react';
 import { css } from 'emotion';
-import { GraphSeriesValue } from '@grafana/data';
+import { GraphSeriesValue, AbsoluteTimeRange } from '@grafana/data';
 
 import { Graph, GraphProps } from './Graph';
 import { LegendRenderOptions, LegendItem, LegendDisplayMode } from '../Legend/Legend';
@@ -18,10 +18,11 @@ export interface GraphWithLegendProps extends GraphProps, LegendRenderOptions {
   displayMode: LegendDisplayMode;
   sortLegendBy?: string;
   sortLegendDesc?: boolean;
-  onSeriesColorChange: SeriesColorChangeHandler;
+  onSeriesColorChange?: SeriesColorChangeHandler;
   onSeriesAxisToggle?: SeriesAxisToggleHandler;
   onSeriesToggle?: (label: string, event: React.MouseEvent<HTMLElement>) => void;
   onToggleSort: (sortBy: string) => void;
+  onSelectionChanged?: (range: AbsoluteTimeRange) => void;
 }
 
 const getGraphWithLegendStyles = ({ placement }: GraphWithLegendProps) => ({
@@ -67,6 +68,10 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
     onToggleSort,
     hideEmpty,
     hideZero,
+    isStacked,
+    lineWidth,
+    onSelectionChanged,
+    timeZone,
   } = props;
   const { graphContainer, wrapper, legendContainer } = getGraphWithLegendStyles(props);
 
@@ -78,7 +83,7 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
             label: s.label,
             color: s.color,
             isVisible: s.isVisible,
-            yAxis: s.yAxis,
+            yAxis: s.yAxis.index,
             displayValues: s.info || [],
           },
         ]);
@@ -90,12 +95,16 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
         <Graph
           series={series.filter(s => !!s.isVisible)}
           timeRange={timeRange}
+          timeZone={timeZone}
           showLines={showLines}
           showPoints={showPoints}
           showBars={showBars}
           width={width}
           height={height}
           key={isLegendVisible ? 'legend-visible' : 'legend-invisible'}
+          isStacked={isStacked}
+          lineWidth={lineWidth}
+          onSelectionChanged={onSelectionChanged}
         />
       </div>
 

+ 11 - 5
packages/grafana-ui/src/components/Graph/mockGraphWithLegendData.ts

@@ -1,7 +1,6 @@
 import { GraphWithLegendProps } from './GraphWithLegend';
 import { LegendDisplayMode } from '../Legend/Legend';
-import { dateTime } from '@grafana/data';
-// import { LegendList } from '../Legend/LegendList';
+import { dateTime, DefaultTimeZone } from '@grafana/data';
 
 export const mockGraphWithLegendData = ({
   displayMode,
@@ -1099,7 +1098,9 @@ export const mockGraphWithLegendData = ({
         { title: 'max', text: '18.42', numeric: 18.427101844163694 },
       ],
       isVisible: true,
-      yAxis: 1,
+      yAxis: {
+        index: 1,
+      },
     },
     {
       label: 'B-series',
@@ -2191,7 +2192,9 @@ export const mockGraphWithLegendData = ({
         { title: 'max', text: '18.42', numeric: 18.427101844163694 },
       ],
       isVisible: true,
-      yAxis: 1,
+      yAxis: {
+        index: 1,
+      },
     },
     {
       label: 'C-series',
@@ -3283,7 +3286,9 @@ export const mockGraphWithLegendData = ({
         { title: 'max', text: '18.42', numeric: 18.427101844163694 },
       ],
       isVisible: true,
-      yAxis: 1,
+      yAxis: {
+        index: 1,
+      },
     },
   ],
   timeRange: {
@@ -3313,4 +3318,5 @@ export const mockGraphWithLegendData = ({
   },
   onToggleSort: () => {},
   displayMode: displayMode || LegendDisplayMode.List,
+  timeZone: DefaultTimeZone,
 });

+ 2 - 1
packages/grafana-ui/src/types/panel.ts

@@ -1,5 +1,5 @@
 import { ComponentClass, ComponentType } from 'react';
-import { LoadingState, DataFrame, TimeRange } from '@grafana/data';
+import { LoadingState, DataFrame, TimeRange, TimeZone } from '@grafana/data';
 import { ScopedVars, DataQueryRequest, DataQueryError, LegacyResponseData } from './datasource';
 import { PluginMeta, GrafanaPlugin } from './plugin';
 
@@ -27,6 +27,7 @@ export interface PanelProps<T = any> {
   // TODO: annotation?: PanelData;
 
   timeRange: TimeRange;
+  timeZone: TimeZone;
   options: T;
   onOptionsChange: (options: T) => void;
   renderCounter: number;

+ 1 - 1
packages/grafana-ui/src/utils/fieldDisplay.ts

@@ -169,7 +169,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
             timeColumn < 0
               ? undefined
               : getFlotPairs({
-                  series,
+                  rows: series.rows,
                   xIndex: timeColumn,
                   yIndex: i,
                   nullValueMode: NullValueMode.Null,

+ 4 - 6
packages/grafana-ui/src/utils/flotPairs.test.ts

@@ -1,12 +1,10 @@
 import { getFlotPairs } from './flotPairs';
 
 describe('getFlotPairs', () => {
-  const series = {
-    fields: [],
-    rows: [[1, 100, 'a'], [2, 200, 'b'], [3, 300, 'c']],
-  };
+  const rows = [[1, 100, 'a'], [2, 200, 'b'], [3, 300, 'c']];
+
   it('should get X and y', () => {
-    const pairs = getFlotPairs({ series, xIndex: 0, yIndex: 1 });
+    const pairs = getFlotPairs({ rows, xIndex: 0, yIndex: 1 });
 
     expect(pairs.length).toEqual(3);
     expect(pairs[0].length).toEqual(2);
@@ -15,7 +13,7 @@ describe('getFlotPairs', () => {
   });
 
   it('should work with strings', () => {
-    const pairs = getFlotPairs({ series, xIndex: 0, yIndex: 2 });
+    const pairs = getFlotPairs({ rows, xIndex: 0, yIndex: 2 });
 
     expect(pairs.length).toEqual(3);
     expect(pairs[0].length).toEqual(2);

+ 3 - 5
packages/grafana-ui/src/utils/flotPairs.ts

@@ -1,16 +1,14 @@
 // Types
-import { NullValueMode, DataFrame, GraphSeriesValue } from '@grafana/data';
+import { NullValueMode, GraphSeriesValue } from '@grafana/data';
 
 export interface FlotPairsOptions {
-  series: DataFrame;
+  rows: any[][];
   xIndex: number;
   yIndex: number;
   nullValueMode?: NullValueMode;
 }
 
-export function getFlotPairs({ series, xIndex, yIndex, nullValueMode }: FlotPairsOptions): GraphSeriesValue[][] {
-  const rows = series.rows;
-
+export function getFlotPairs({ rows, xIndex, yIndex, nullValueMode }: FlotPairsOptions): GraphSeriesValue[][] {
   const ignoreNulls = nullValueMode === NullValueMode.Ignore;
   const nullAsZero = nullValueMode === NullValueMode.AsZero;
 

+ 36 - 13
public/app/core/logs_model.ts

@@ -1,17 +1,15 @@
 import _ from 'lodash';
 import ansicolor from 'vendor/ansicolor/ansicolor';
 
-import { colors } from '@grafana/ui';
+import { colors, getFlotPairs } from '@grafana/ui';
 
 import {
-  TimeSeries,
   Labels,
   LogLevel,
   DataFrame,
   findCommonLabels,
   findUniqueLabels,
   getLogLevel,
-  toLegacyResponseData,
   FieldCache,
   FieldType,
   getLogLevelFromKey,
@@ -22,10 +20,15 @@ import {
   LogsParser,
   LogLabelStatsModel,
   LogsDedupStrategy,
+  GraphSeriesXY,
+  LoadingState,
+  dateTime,
+  toUtc,
+  NullValueMode,
 } from '@grafana/data';
 import { getThemeColor } from 'app/core/utils/colors';
 import { hasAnsiCodes } from 'app/core/utils/text';
-import { dateTime, toUtc } from '@grafana/data';
+import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel';
 
 export const LogLevelColor = {
   [LogLevel.critical]: colors[7],
@@ -192,7 +195,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>)
   };
 }
 
-export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): TimeSeries[] {
+export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): GraphSeriesXY[] {
   // currently interval is rangeMs / resolution, which is too low for showing series as bars.
   // need at least 10px per bucket, so we multiply interval by 10. Should be solved higher up the chain
   // when executing queries & interval calculated and not here but this is a temporary fix.
@@ -242,12 +245,26 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): Time
       return a[1] - b[1];
     });
 
-    return {
-      datapoints: series.datapoints,
-      target: series.alias,
-      alias: series.alias,
+    const points = getFlotPairs({
+      rows: series.datapoints,
+      xIndex: 1,
+      yIndex: 0,
+      nullValueMode: NullValueMode.Null,
+    });
+
+    const graphSeries: GraphSeriesXY = {
       color: series.color,
+      label: series.alias,
+      data: points,
+      isVisible: true,
+      yAxis: {
+        index: 1,
+        min: 0,
+        tickDecimals: 0,
+      },
     };
+
+    return graphSeries;
   });
 }
 
@@ -273,10 +290,16 @@ export function dataFrameToLogsModel(dataFrame: DataFrame[], intervalMs: number)
     if (metricSeries.length === 0) {
       logsModel.series = makeSeriesForLogs(logsModel.rows, intervalMs);
     } else {
-      logsModel.series = [];
-      for (const series of metricSeries) {
-        logsModel.series.push(toLegacyResponseData(series) as TimeSeries);
-      }
+      logsModel.series = getGraphSeriesModel(
+        { series: metricSeries, state: LoadingState.Done },
+        {},
+        { showBars: true, showLines: false, showPoints: false },
+        {
+          asTable: false,
+          isVisible: true,
+          placement: 'under',
+        }
+      );
     }
 
     return logsModel;

+ 2 - 1
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -178,7 +178,7 @@ export class PanelChrome extends PureComponent<Props, State> {
         queries: panel.targets,
         panelId: panel.id,
         dashboardId: this.props.dashboard.id,
-        timezone: this.props.dashboard.timezone,
+        timezone: this.props.dashboard.getTimezone(),
         timeRange: timeData.timeRange,
         timeInfo: timeData.timeInfo,
         widthPixels: width,
@@ -251,6 +251,7 @@ export class PanelChrome extends PureComponent<Props, State> {
             id={panel.id}
             data={data}
             timeRange={data.request ? data.request.range : this.timeSrv.timeRange()}
+            timeZone={this.props.dashboard.getTimezone()}
             options={panel.getOptions()}
             transparent={panel.transparent}
             width={width - theme.panelPadding * 2}

+ 14 - 8
public/app/features/explore/Explore.tsx

@@ -12,7 +12,6 @@ import store from 'app/core/store';
 // Components
 import { Alert } from './Error';
 import ErrorBoundary from './ErrorBoundary';
-import GraphContainer from './GraphContainer';
 import LogsContainer from './LogsContainer';
 import QueryRows from './QueryRows';
 import TableContainer from './TableContainer';
@@ -30,7 +29,7 @@ import {
 } from './state/actions';
 
 // Types
-import { RawTimeRange } from '@grafana/data';
+import { RawTimeRange, GraphSeriesXY } from '@grafana/data';
 
 import { DataQuery, ExploreStartPageProps, DataSourceApi, DataQueryError } from '@grafana/ui';
 import {
@@ -56,6 +55,7 @@ import { FadeIn } from 'app/core/components/Animations/FadeIn';
 import { getTimeZone } from '../profile/state/selectors';
 import { ErrorContainer } from './ErrorContainer';
 import { scanStopAction } from './state/actionTypes';
+import ExploreGraphPanel from './ExploreGraphPanel';
 
 interface ExploreProps {
   StartPage?: ComponentClass<ExploreStartPageProps>;
@@ -87,6 +87,7 @@ interface ExploreProps {
   queryErrors: DataQueryError[];
   isLive: boolean;
   updateTimeRange: typeof updateTimeRange;
+  graphResult?: GraphSeriesXY[];
 }
 
 /**
@@ -192,7 +193,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
   refreshExplore = () => {
     const { exploreId, update } = this.props;
 
-    if (update.queries || update.ui || update.range || update.datasource) {
+    if (update.queries || update.ui || update.range || update.datasource || update.mode) {
       this.props.refreshExplore(exploreId);
     }
   };
@@ -225,6 +226,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
       queryKeys,
       queryErrors,
       mode,
+      graphResult,
     } = this.props;
     const exploreClass = split ? 'explore explore-split' : 'explore';
 
@@ -259,7 +261,9 @@ export class Explore extends React.PureComponent<ExploreProps> {
                       {showingStartPage && <StartPage onClickExample={this.onClickExample} />}
                       {!showingStartPage && (
                         <>
-                          {mode === ExploreMode.Metrics && <GraphContainer width={width} exploreId={exploreId} />}
+                          {mode === ExploreMode.Metrics && (
+                            <ExploreGraphPanel exploreId={exploreId} series={graphResult} width={width} />
+                          )}
                           {mode === ExploreMode.Metrics && (
                             <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />
                           )}
@@ -306,6 +310,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
     isLive,
     supportedModes,
     mode,
+    graphResult,
   } = item;
 
   const { datasource, queries, range: urlRange, mode: urlMode, ui } = (urlState || {}) as ExploreUrlState;
@@ -318,15 +323,15 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
     const urlModeIsValid = supportedModes.includes(urlMode);
     const modeStateIsValid = supportedModes.includes(mode);
 
-    if (urlModeIsValid) {
-      newMode = urlMode;
-    } else if (modeStateIsValid) {
+    if (modeStateIsValid) {
       newMode = mode;
+    } else if (urlModeIsValid) {
+      newMode = urlMode;
     } else {
       newMode = supportedModes[0];
     }
   } else {
-    newMode = [ExploreMode.Metrics, ExploreMode.Logs].includes(urlMode) ? urlMode : ExploreMode.Metrics;
+    newMode = [ExploreMode.Metrics, ExploreMode.Logs].includes(mode) ? mode : ExploreMode.Metrics;
   }
 
   const initialUI = ui || DEFAULT_UI_STATE;
@@ -349,6 +354,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
     initialUI,
     queryErrors,
     isLive,
+    graphResult,
   };
 }
 

+ 173 - 0
public/app/features/explore/ExploreGraphPanel.tsx

@@ -0,0 +1,173 @@
+import React, { PureComponent } from 'react';
+import { hot } from 'react-hot-loader';
+import { connect } from 'react-redux';
+import { LegendDisplayMode, GraphWithLegend } from '@grafana/ui';
+import { TimeZone, AbsoluteTimeRange, GraphSeriesXY, dateTimeForTimeZone, LoadingState } from '@grafana/data';
+
+import { GraphSeriesToggler } from 'app/plugins/panel/graph2/GraphSeriesToggler';
+import Panel from './Panel';
+import { StoreState, ExploreId, ExploreMode } from 'app/types';
+import { getTimeZone } from '../profile/state/selectors';
+import { toggleGraph, updateTimeRange } from './state/actions';
+
+const MAX_NUMBER_OF_TIME_SERIES = 20;
+
+interface Props {
+  exploreId: ExploreId;
+  series: GraphSeriesXY[];
+  width: number;
+  absoluteRange?: AbsoluteTimeRange;
+  loading?: boolean;
+  mode?: ExploreMode;
+  showingGraph?: boolean;
+  showingTable?: boolean;
+  timeZone?: TimeZone;
+  onHiddenSeriesChanged?: (hiddenSeries: string[]) => void;
+  toggleGraph: typeof toggleGraph;
+  updateTimeRange: typeof updateTimeRange;
+}
+
+interface State {
+  hiddenSeries: string[];
+  showAllTimeSeries: boolean;
+}
+
+export class ExploreGraphPanel extends PureComponent<Props, State> {
+  state: State = {
+    hiddenSeries: [],
+    showAllTimeSeries: false,
+  };
+
+  onShowAllTimeSeries = () => {
+    this.setState({
+      showAllTimeSeries: true,
+    });
+  };
+
+  onClickGraphButton = () => {
+    const { toggleGraph, exploreId, showingGraph } = this.props;
+    toggleGraph(exploreId, showingGraph);
+  };
+
+  onChangeTime = (absoluteRange: AbsoluteTimeRange) => {
+    const { exploreId, updateTimeRange } = this.props;
+
+    updateTimeRange({ exploreId, absoluteRange });
+  };
+
+  renderGraph = () => {
+    const {
+      width,
+      series,
+      onHiddenSeriesChanged,
+      timeZone,
+      absoluteRange,
+      mode,
+      showingGraph,
+      showingTable,
+    } = this.props;
+    const { showAllTimeSeries } = this.state;
+
+    if (!series) {
+      return null;
+    }
+
+    const timeRange = {
+      from: dateTimeForTimeZone(timeZone, absoluteRange.from),
+      to: dateTimeForTimeZone(timeZone, absoluteRange.to),
+      raw: {
+        from: dateTimeForTimeZone(timeZone, absoluteRange.from),
+        to: dateTimeForTimeZone(timeZone, absoluteRange.to),
+      },
+    };
+    const height = mode === ExploreMode.Logs ? 100 : showingGraph && showingTable ? 200 : 400;
+    const showBars = mode === ExploreMode.Logs ? true : false;
+    const showLines = mode === ExploreMode.Metrics ? true : false;
+    const isStacked = mode === ExploreMode.Logs ? true : false;
+    const lineWidth = mode === ExploreMode.Metrics ? 1 : 5;
+    const seriesToShow = showAllTimeSeries ? series : series.slice(0, MAX_NUMBER_OF_TIME_SERIES);
+
+    return (
+      <GraphSeriesToggler series={seriesToShow} onHiddenSeriesChanged={onHiddenSeriesChanged}>
+        {({ onSeriesToggle, toggledSeries }) => {
+          return (
+            <GraphWithLegend
+              displayMode={LegendDisplayMode.List}
+              height={height}
+              isLegendVisible={true}
+              placement={'under'}
+              width={width}
+              timeRange={timeRange}
+              timeZone={timeZone}
+              showBars={showBars}
+              showLines={showLines}
+              showPoints={false}
+              onToggleSort={() => {}}
+              series={toggledSeries}
+              isStacked={isStacked}
+              lineWidth={lineWidth}
+              onSeriesToggle={onSeriesToggle}
+              onSelectionChanged={this.onChangeTime}
+            />
+          );
+        }}
+      </GraphSeriesToggler>
+    );
+  };
+
+  render() {
+    const { series, mode, showingGraph, loading } = this.props;
+    const { showAllTimeSeries } = this.state;
+
+    return (
+      <>
+        {series && series.length > MAX_NUMBER_OF_TIME_SERIES && !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 ${
+              series.length
+            }`}</span>
+          </div>
+        )}
+
+        {mode === ExploreMode.Metrics && (
+          <Panel label="Graph" collapsible isOpen={showingGraph} loading={loading} onToggle={this.onClickGraphButton}>
+            {this.renderGraph()}
+          </Panel>
+        )}
+
+        {mode === ExploreMode.Logs && this.renderGraph()}
+      </>
+    );
+  }
+}
+
+function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }) {
+  const explore = state.explore;
+  // @ts-ignore
+  const item: ExploreItemState = explore[exploreId];
+  const { loadingState, showingGraph, showingTable, absoluteRange, mode } = item;
+  const loading = loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming;
+
+  return {
+    loading,
+    showingGraph,
+    showingTable,
+    timeZone: getTimeZone(state.user),
+    absoluteRange,
+    mode,
+  };
+}
+
+const mapDispatchToProps = {
+  toggleGraph,
+  updateTimeRange,
+};
+
+export default hot(module)(
+  connect(
+    mapStateToProps,
+    mapDispatchToProps
+  )(ExploreGraphPanel)
+);

+ 0 - 50
public/app/features/explore/Graph.test.tsx

@@ -1,50 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import { Graph } from './Graph';
-import { mockData } from './__mocks__/mockData';
-import { DefaultTimeZone } from '@grafana/data';
-
-const setup = (propOverrides?: object) => {
-  const props = {
-    size: { width: 10, height: 20 },
-    data: mockData().slice(0, 19),
-    range: { from: 0, to: 1 },
-    timeZone: DefaultTimeZone,
-    ...propOverrides,
-  };
-
-  // Enzyme.shallow did not work well with jquery.flop. Mocking the draw function.
-  Graph.prototype.draw = jest.fn();
-
-  const wrapper = shallow(<Graph {...props} />);
-  const instance = wrapper.instance() as Graph;
-
-  return {
-    wrapper,
-    instance,
-  };
-};
-
-describe('Render', () => {
-  it('should render component', () => {
-    const { wrapper } = setup();
-
-    expect(wrapper).toMatchSnapshot();
-  });
-
-  it('should render component with disclaimer', () => {
-    const { wrapper } = setup({
-      data: mockData(),
-    });
-
-    expect(wrapper).toMatchSnapshot();
-  });
-
-  it('should show query return no time series', () => {
-    const { wrapper } = setup({
-      data: [],
-    });
-
-    expect(wrapper).toMatchSnapshot();
-  });
-});

+ 0 - 280
public/app/features/explore/Graph.tsx

@@ -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;

+ 0 - 100
public/app/features/explore/GraphContainer.tsx

@@ -1,100 +0,0 @@
-import React, { PureComponent } from 'react';
-import { hot } from 'react-hot-loader';
-import { connect } from 'react-redux';
-import { TimeZone, AbsoluteTimeRange, LoadingState } from '@grafana/data';
-
-import { ExploreId, ExploreItemState } from 'app/types/explore';
-import { StoreState } from 'app/types';
-
-import { toggleGraph, updateTimeRange } from './state/actions';
-import Graph from './Graph';
-import Panel from './Panel';
-import { getTimeZone } from '../profile/state/selectors';
-
-interface GraphContainerProps {
-  exploreId: ExploreId;
-  graphResult?: any[];
-  loading: boolean;
-  absoluteRange: AbsoluteTimeRange;
-  timeZone: TimeZone;
-  showingGraph: boolean;
-  showingTable: boolean;
-  split: boolean;
-  toggleGraph: typeof toggleGraph;
-  updateTimeRange: typeof updateTimeRange;
-  width: number;
-}
-
-export class GraphContainer extends PureComponent<GraphContainerProps> {
-  onClickGraphButton = () => {
-    this.props.toggleGraph(this.props.exploreId, this.props.showingGraph);
-  };
-
-  onChangeTime = (absoluteRange: AbsoluteTimeRange) => {
-    const { exploreId, updateTimeRange } = this.props;
-
-    updateTimeRange({ exploreId, absoluteRange });
-  };
-
-  render() {
-    const {
-      exploreId,
-      graphResult,
-      loading,
-      showingGraph,
-      showingTable,
-      absoluteRange,
-      split,
-      width,
-      timeZone,
-    } = this.props;
-    const graphHeight = showingGraph && showingTable ? 200 : 400;
-
-    return (
-      <Panel label="Graph" collapsible isOpen={showingGraph} loading={loading} onToggle={this.onClickGraphButton}>
-        {graphResult && (
-          <Graph
-            data={graphResult}
-            height={graphHeight}
-            id={`explore-graph-${exploreId}`}
-            onChangeTime={this.onChangeTime}
-            range={absoluteRange}
-            timeZone={timeZone}
-            split={split}
-            width={width}
-          />
-        )}
-      </Panel>
-    );
-  }
-}
-
-function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }) {
-  const explore = state.explore;
-  const { split } = explore;
-  // @ts-ignore
-  const item: ExploreItemState = explore[exploreId];
-  const { graphResult, loadingState, showingGraph, showingTable, absoluteRange } = item;
-  const loading = loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming;
-  return {
-    graphResult,
-    loading,
-    showingGraph,
-    showingTable,
-    split,
-    timeZone: getTimeZone(state.user),
-    absoluteRange,
-  };
-}
-
-const mapDispatchToProps = {
-  toggleGraph,
-  updateTimeRange,
-};
-
-export default hot(module)(
-  connect(
-    mapStateToProps,
-    mapDispatchToProps
-  )(GraphContainer)
-);

+ 8 - 32
public/app/features/explore/Logs.tsx

@@ -13,32 +13,17 @@ import {
   LogsDedupStrategy,
   LogRowModel,
 } from '@grafana/data';
-import TimeSeries from 'app/core/time_series2';
 
 import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
 
-import Graph from './Graph';
 import { LogLabels } from './LogLabels';
 import { LogRow } from './LogRow';
 import { LogsDedupDescription } from 'app/core/logs_model';
+import ExploreGraphPanel from './ExploreGraphPanel';
+import { ExploreId } from 'app/types';
 
 const PREVIEW_LIMIT = 100;
 
-const graphOptions = {
-  series: {
-    stack: true,
-    bars: {
-      show: true,
-      lineWidth: 5,
-      // barWidth: 10,
-    },
-    // stack: true,
-  },
-  yaxis: {
-    tickDecimals: 0,
-  },
-};
-
 function renderMetaItem(value: any, kind: LogsMetaKind) {
   if (kind === LogsMetaKind.LabelsMap) {
     return (
@@ -54,7 +39,7 @@ interface Props {
   data?: LogsModel;
   dedupedData?: LogsModel;
   width: number;
-  exploreId: string;
+  exploreId: ExploreId;
   highlighterExpressions: string[];
   loading: boolean;
   absoluteRange: AbsoluteTimeRange;
@@ -135,7 +120,7 @@ export default class Logs extends PureComponent<Props, State> {
     });
   };
 
-  onToggleLogLevel = (rawLevel: string, hiddenRawLevels: string[]) => {
+  onToggleLogLevel = (hiddenRawLevels: string[]) => {
     const hiddenLogLevels: LogLevel[] = hiddenRawLevels.map((level: LogLevel) => LogLevel[level]);
     this.props.onToggleLogLevel(hiddenLogLevels);
   };
@@ -157,7 +142,6 @@ export default class Logs extends PureComponent<Props, State> {
       highlighterExpressions,
       loading = false,
       onClickLabel,
-      absoluteRange,
       timeZone,
       scanning,
       scanRange,
@@ -193,23 +177,15 @@ export default class Logs extends PureComponent<Props, State> {
 
     // React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead
     const getRows = () => processedRows;
-    const timeSeries = data.series
-      ? data.series.map(series => new TimeSeries(series))
-      : [new TimeSeries({ datapoints: [] })];
 
     return (
       <div className="logs-panel">
         <div className="logs-panel-graph">
-          <Graph
-            data={timeSeries}
-            height={100}
+          <ExploreGraphPanel
+            exploreId={exploreId}
+            series={data.series}
             width={width}
-            range={absoluteRange}
-            timeZone={timeZone}
-            id={`explore-logs-graph-${exploreId}`}
-            onChangeTime={this.props.onChangeTime}
-            onToggleSeries={this.onToggleLogLevel}
-            userOptions={graphOptions}
+            onHiddenSeriesChanged={this.onToggleLogLevel}
           />
         </div>
         <div className="logs-panel-options">

+ 2 - 2
public/app/features/explore/QueryRow.tsx

@@ -13,7 +13,7 @@ import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/act
 
 // Types
 import { StoreState } from 'app/types';
-import { TimeRange, AbsoluteTimeRange } from '@grafana/data';
+import { TimeRange, AbsoluteTimeRange, toDataFrame, guessFieldTypes } from '@grafana/data';
 import { DataQuery, DataSourceApi, QueryFixAction, DataSourceStatus, PanelData, DataQueryError } from '@grafana/ui';
 import { HistoryItem, ExploreItemState, ExploreId, ExploreMode } from 'app/types/explore';
 import { Emitter } from 'app/core/utils/emitter';
@@ -217,7 +217,7 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
   const query = queries[index];
   const datasourceStatus = datasourceError ? DataSourceStatus.Disconnected : DataSourceStatus.Connected;
   const error = queryErrors.filter(queryError => queryError.refId === query.refId)[0];
-  const series = graphResult ? graphResult : []; // TODO: use DataFrame
+  const series = graphResult ? graphResult.map(serie => guessFieldTypes(toDataFrame(serie))) : []; // TODO: use DataFrame
   const queryResponse: PanelData = {
     series,
     state: loadingState,

+ 0 - 315
public/app/features/explore/__snapshots__/Graph.test.tsx.snap

@@ -1,315 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Render should render component 1`] = `
-<Fragment>
-  <div
-    className="explore-graph"
-    id="graph"
-    style={
-      Object {
-        "height": 100,
-      }
-    }
-  />
-  <GraphLegend
-    displayMode="list"
-    items={
-      Array [
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-      ]
-    }
-    onLabelClick={[Function]}
-    placement="under"
-  />
-</Fragment>
-`;
-
-exports[`Render should render component with disclaimer 1`] = `
-<Fragment>
-  <div
-    className="time-series-disclaimer"
-  >
-    <i
-      className="fa fa-fw fa-warning disclaimer-icon"
-    />
-    Showing only 20 time series. 
-    <span
-      className="show-all-time-series"
-      onClick={[Function]}
-    >
-      Show all 27
-    </span>
-  </div>
-  <div
-    className="explore-graph"
-    id="graph"
-    style={
-      Object {
-        "height": 100,
-      }
-    }
-  />
-  <GraphLegend
-    displayMode="list"
-    items={
-      Array [
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-        Object {
-          "color": undefined,
-          "isVisible": true,
-          "label": undefined,
-          "yAxis": 1,
-        },
-      ]
-    }
-    onLabelClick={[Function]}
-    placement="under"
-  />
-</Fragment>
-`;
-
-exports[`Render should show query return no time series 1`] = `
-<Fragment>
-  <div
-    className="explore-graph"
-    id="graph"
-    style={
-      Object {
-        "height": 100,
-      }
-    }
-  />
-  <GraphLegend
-    displayMode="list"
-    items={Array []}
-    onLabelClick={[Function]}
-    placement="under"
-  />
-</Fragment>
-`;

+ 2 - 2
public/app/features/explore/state/actionTypes.ts

@@ -15,9 +15,9 @@ import {
   TimeRange,
   DataFrame,
   LogsModel,
-  TimeSeries,
   LoadingState,
   AbsoluteTimeRange,
+  GraphSeriesXY,
 } from '@grafana/data';
 import { ExploreId, ExploreItemState, HistoryItem, ExploreUIState, ExploreMode, QueryOptions } from 'app/types/explore';
 import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
@@ -149,7 +149,7 @@ export interface QuerySuccessPayload {
   exploreId: ExploreId;
   latency: number;
   loadingState: LoadingState;
-  graphResult: TimeSeries[];
+  graphResult: GraphSeriesXY[];
   tableResult: TableModel;
   logsResult: LogsModel;
 }

+ 48 - 77
public/app/features/explore/utils/ResultProcessor.test.ts

@@ -16,8 +16,7 @@ jest.mock('@grafana/data/src/utils/moment_wrapper', () => ({
 import { ResultProcessor } from './ResultProcessor';
 import { ExploreItemState, ExploreMode } from 'app/types/explore';
 import TableModel from 'app/core/table_model';
-import { toFixed } from '@grafana/ui';
-import { TimeSeries, LogRowModel, LogsMetaItem } from '@grafana/data';
+import { TimeSeries, LogRowModel, LogsMetaItem, GraphSeriesXY } from '@grafana/data';
 
 const testContext = (options: any = {}) => {
   const response = [
@@ -129,20 +128,14 @@ describe('ResultProcessor', () => {
 
         expect(theResult).toEqual([
           {
-            alias: 'A-series',
-            aliasEscaped: 'A-series',
-            bars: {
-              fillColor: '#7EB26D',
-            },
-            hasMsResolution: true,
-            id: 'A-series',
             label: 'A-series',
-            legend: true,
-            stats: {},
             color: '#7EB26D',
-            datapoints: [[39.91264531864214, 1559038518831], [40.35179822906545, 1559038519831]],
-            unit: undefined,
-            valueFormater: toFixed,
+            data: [[1559038518831, 39.91264531864214], [1559038519831, 40.35179822906545]],
+            info: undefined,
+            isVisible: true,
+            yAxis: {
+              index: 1,
+            },
           },
         ]);
       });
@@ -205,12 +198,14 @@ describe('ResultProcessor', () => {
           ],
           series: [
             {
-              alias: 'A-series',
-              datapoints: [[39.91264531864214, 1559038518831], [40.35179822906545, 1559038519831]],
-              meta: undefined,
-              refId: 'A',
-              target: 'A-series',
-              unit: undefined,
+              label: 'A-series',
+              color: '#7EB26D',
+              data: [[1559038518831, 39.91264531864214], [1559038519831, 40.35179822906545]],
+              info: undefined,
+              isVisible: true,
+              yAxis: {
+                index: 1,
+              },
             },
           ],
         });
@@ -234,20 +229,14 @@ describe('ResultProcessor', () => {
           replacePreviousResults: false,
           graphResult: [
             {
-              alias: 'A-series',
-              aliasEscaped: 'A-series',
-              bars: {
-                fillColor: '#7EB26D',
-              },
-              hasMsResolution: true,
-              id: 'A-series',
               label: 'A-series',
-              legend: true,
-              stats: {},
               color: '#7EB26D',
-              datapoints: [[19.91264531864214, 1558038518831], [20.35179822906545, 1558038519831]],
-              unit: undefined,
-              valueFormater: toFixed,
+              data: [[1558038518831, 19.91264531864214], [1558038518831, 20.35179822906545]],
+              info: undefined,
+              isVisible: true,
+              yAxis: {
+                index: 1,
+              },
             },
           ],
         });
@@ -255,25 +244,19 @@ describe('ResultProcessor', () => {
 
         expect(theResult).toEqual([
           {
-            alias: 'A-series',
-            aliasEscaped: 'A-series',
-            bars: {
-              fillColor: '#7EB26D',
-            },
-            hasMsResolution: true,
-            id: 'A-series',
             label: 'A-series',
-            legend: true,
-            stats: {},
             color: '#7EB26D',
-            datapoints: [
-              [19.91264531864214, 1558038518831],
-              [20.35179822906545, 1558038519831],
-              [39.91264531864214, 1559038518831],
-              [40.35179822906545, 1559038519831],
+            data: [
+              [1558038518831, 19.91264531864214],
+              [1558038518831, 20.35179822906545],
+              [1559038518831, 39.91264531864214],
+              [1559038519831, 40.35179822906545],
             ],
-            unit: undefined,
-            valueFormater: toFixed,
+            info: undefined,
+            isVisible: true,
+            yAxis: {
+              index: 1,
+            },
           },
         ]);
       });
@@ -351,20 +334,14 @@ describe('ResultProcessor', () => {
             ],
             series: [
               {
-                alias: 'A-series',
-                aliasEscaped: 'A-series',
-                bars: {
-                  fillColor: '#7EB26D',
-                },
-                hasMsResolution: true,
-                id: 'A-series',
                 label: 'A-series',
-                legend: true,
-                stats: {},
                 color: '#7EB26D',
-                datapoints: [[37.91264531864214, 1558038518831], [38.35179822906545, 1558038519831]],
-                unit: undefined,
-                valueFormater: toFixed,
+                data: [[1558038518831, 37.91264531864214], [1558038519831, 38.35179822906545]],
+                info: undefined,
+                isVisible: true,
+                yAxis: {
+                  index: 1,
+                },
               },
             ],
           },
@@ -437,26 +414,20 @@ describe('ResultProcessor', () => {
           ],
           series: [
             {
-              alias: 'A-series',
-              aliasEscaped: 'A-series',
-              bars: {
-                fillColor: '#7EB26D',
-              },
-              hasMsResolution: true,
-              id: 'A-series',
               label: 'A-series',
-              legend: true,
-              stats: {},
               color: '#7EB26D',
-              datapoints: [
-                [37.91264531864214, 1558038518831],
-                [38.35179822906545, 1558038519831],
-                [39.91264531864214, 1559038518831],
-                [40.35179822906545, 1559038519831],
+              data: [
+                [1558038518831, 37.91264531864214],
+                [1558038519831, 38.35179822906545],
+                [1559038518831, 39.91264531864214],
+                [1559038519831, 40.35179822906545],
               ],
-              unit: undefined as string,
-              valueFormater: toFixed,
-            },
+              info: undefined,
+              isVisible: true,
+              yAxis: {
+                index: 1,
+              },
+            } as GraphSeriesXY,
           ],
         };
 

+ 37 - 35
public/app/features/explore/utils/ResultProcessor.ts

@@ -1,14 +1,22 @@
 import { DataQueryResponse, DataQueryResponseData } from '@grafana/ui';
 
-import { TableData, isTableData, LogsModel, toDataFrame, guessFieldTypes, TimeSeries } from '@grafana/data';
+import {
+  TableData,
+  isTableData,
+  LogsModel,
+  toDataFrame,
+  guessFieldTypes,
+  TimeSeries,
+  GraphSeriesXY,
+  LoadingState,
+} from '@grafana/data';
 
 import { ExploreItemState, ExploreMode } from 'app/types/explore';
 import { getProcessedDataFrames } from 'app/features/dashboard/state/PanelQueryState';
 import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
 import { sortLogsResult } from 'app/core/utils/explore';
 import { dataFrameToLogsModel } from 'app/core/logs_model';
-import { default as TimeSeries2 } from 'app/core/time_series2';
-import { DataProcessor } from 'app/plugins/panel/graph/data_processor';
+import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel';
 
 export class ResultProcessor {
   private rawData: DataQueryResponseData[] = [];
@@ -45,12 +53,12 @@ export class ResultProcessor {
     return this.rawData;
   };
 
-  getGraphResult = (): TimeSeries[] => {
+  getGraphResult = (): GraphSeriesXY[] => {
     if (this.state.mode !== ExploreMode.Metrics) {
       return [];
     }
 
-    const newResults = this.makeTimeSeriesList(this.metrics);
+    const newResults = this.createGraphSeries(this.metrics);
     return this.mergeGraphResults(newResults, this.state.graphResult);
   };
 
@@ -100,26 +108,26 @@ export class ResultProcessor {
     return { ...sortedNewResults, rows, series };
   };
 
-  private makeTimeSeriesList = (rawData: any[]) => {
-    const dataList = getProcessedDataFrames(rawData);
-    const dataProcessor = new DataProcessor({ xaxis: {}, aliasColors: [] }); // Hack before we use GraphSeriesXY instead
-    const timeSeries = dataProcessor.getSeriesList({ dataList });
+  private createGraphSeries = (rawData: any[]) => {
+    const dataFrames = getProcessedDataFrames(rawData);
+    const graphSeries = getGraphSeriesModel(
+      { series: dataFrames, state: LoadingState.Done },
+      {},
+      { showBars: false, showLines: true, showPoints: false },
+      {
+        asTable: false,
+        isVisible: true,
+        placement: 'under',
+      }
+    );
 
-    return (timeSeries as any) as TimeSeries[]; // Hack before we use GraphSeriesXY instead
+    return graphSeries;
   };
 
-  private isSameTimeSeries = (a: TimeSeries | TimeSeries2, b: TimeSeries | TimeSeries2) => {
-    if (a.hasOwnProperty('id') && b.hasOwnProperty('id')) {
-      const aValue = (a as TimeSeries2).id;
-      const bValue = (b as TimeSeries2).id;
-      if (aValue !== undefined && bValue !== undefined && aValue === bValue) {
-        return true;
-      }
-    }
-
-    if (a.hasOwnProperty('alias') && b.hasOwnProperty('alias')) {
-      const aValue = (a as TimeSeries2).alias;
-      const bValue = (b as TimeSeries2).alias;
+  private isSameGraphSeries = (a: GraphSeriesXY, b: GraphSeriesXY) => {
+    if (a.hasOwnProperty('label') && b.hasOwnProperty('label')) {
+      const aValue = a.label;
+      const bValue = b.label;
       if (aValue !== undefined && bValue !== undefined && aValue === bValue) {
         return true;
       }
@@ -128,24 +136,21 @@ export class ResultProcessor {
     return false;
   };
 
-  private mergeGraphResults = (
-    newResults: TimeSeries[] | TimeSeries2[],
-    prevResults: TimeSeries[] | TimeSeries2[]
-  ): TimeSeries[] => {
+  private mergeGraphResults = (newResults: GraphSeriesXY[], prevResults: GraphSeriesXY[]): GraphSeriesXY[] => {
     if (!prevResults || prevResults.length === 0 || this.replacePreviousResults) {
-      return (newResults as any) as TimeSeries[]; // Hack before we use GraphSeriesXY instead
+      return newResults; // Hack before we use GraphSeriesXY instead
     }
 
-    const results: TimeSeries[] = prevResults.slice() as TimeSeries[];
+    const results: GraphSeriesXY[] = prevResults.slice() as GraphSeriesXY[];
 
     // update existing results
     for (let index = 0; index < results.length; index++) {
       const prevResult = results[index];
       for (const newResult of newResults) {
-        const isSame = this.isSameTimeSeries(prevResult, newResult);
+        const isSame = this.isSameGraphSeries(prevResult, newResult);
 
         if (isSame) {
-          prevResult.datapoints = prevResult.datapoints.concat(newResult.datapoints);
+          prevResult.data = prevResult.data.concat(newResult.data);
           break;
         }
       }
@@ -155,7 +160,7 @@ export class ResultProcessor {
     for (const newResult of newResults) {
       let isNew = true;
       for (const prevResult of results) {
-        const isSame = this.isSameTimeSeries(prevResult, newResult);
+        const isSame = this.isSameGraphSeries(prevResult, newResult);
         if (isSame) {
           isNew = false;
           break;
@@ -163,10 +168,7 @@ export class ResultProcessor {
       }
 
       if (isNew) {
-        const timeSeries2Result = new TimeSeries2({ ...newResult });
-
-        const result = (timeSeries2Result as any) as TimeSeries; // Hack before we use GraphSeriesXY instead
-        results.push(result);
+        results.push(newResult);
       }
     }
     return results;

+ 2 - 0
public/app/plugins/panel/graph2/GraphPanel.tsx

@@ -9,6 +9,7 @@ interface GraphPanelProps extends PanelProps<Options> {}
 export const GraphPanel: React.FunctionComponent<GraphPanelProps> = ({
   data,
   timeRange,
+  timeZone,
   width,
   height,
   options,
@@ -39,6 +40,7 @@ export const GraphPanel: React.FunctionComponent<GraphPanelProps> = ({
         return (
           <GraphWithLegend
             timeRange={timeRange}
+            timeZone={timeZone}
             width={width}
             height={height}
             displayMode={asTable ? LegendDisplayMode.Table : LegendDisplayMode.List}

+ 23 - 46
public/app/plugins/panel/graph2/GraphPanelController.tsx

@@ -1,11 +1,11 @@
 import React from 'react';
 import { PanelData } from '@grafana/ui';
 import { GraphSeriesXY } from '@grafana/data';
-import difference from 'lodash/difference';
 
 import { getGraphSeriesModel } from './getGraphSeriesModel';
 import { Options, SeriesOptions } from './types';
 import { SeriesColorChangeHandler, SeriesAxisToggleHandler } from '@grafana/ui/src/components/Graph/GraphWithLegend';
+import { GraphSeriesToggler } from './GraphSeriesToggler';
 
 interface GraphPanelControllerAPI {
   series: GraphSeriesXY[];
@@ -24,14 +24,12 @@ interface GraphPanelControllerProps {
 
 interface GraphPanelControllerState {
   graphSeriesModel: GraphSeriesXY[];
-  hiddenSeries: string[];
 }
 
 export class GraphPanelController extends React.Component<GraphPanelControllerProps, GraphPanelControllerState> {
   constructor(props: GraphPanelControllerProps) {
     super(props);
 
-    this.onSeriesToggle = this.onSeriesToggle.bind(this);
     this.onSeriesColorChange = this.onSeriesColorChange.bind(this);
     this.onSeriesAxisToggle = this.onSeriesAxisToggle.bind(this);
     this.onToggleSort = this.onToggleSort.bind(this);
@@ -43,7 +41,6 @@ export class GraphPanelController extends React.Component<GraphPanelControllerPr
         props.options.graph,
         props.options.legend
       ),
-      hiddenSeries: [],
     };
   }
 
@@ -76,10 +73,15 @@ export class GraphPanelController extends React.Component<GraphPanelControllerPr
     const seriesOptionsUpdate: SeriesOptions = series[label]
       ? {
           ...series[label],
-          yAxis,
+          yAxis: {
+            ...series[label].yAxis,
+            index: yAxis,
+          },
         }
       : {
-          yAxis,
+          yAxis: {
+            index: yAxis,
+          },
         };
     this.onSeriesOptionsUpdate(label, seriesOptionsUpdate);
   }
@@ -112,47 +114,22 @@ export class GraphPanelController extends React.Component<GraphPanelControllerPr
     });
   }
 
-  onSeriesToggle(label: string, event: React.MouseEvent<HTMLElement>) {
-    const { hiddenSeries, graphSeriesModel } = this.state;
-
-    if (event.ctrlKey || event.metaKey || event.shiftKey) {
-      // Toggling series with key makes the series itself to toggle
-      if (hiddenSeries.indexOf(label) > -1) {
-        this.setState({
-          hiddenSeries: hiddenSeries.filter(series => series !== label),
-        });
-      } else {
-        this.setState({
-          hiddenSeries: hiddenSeries.concat([label]),
-        });
-      }
-    } else {
-      // Toggling series with out key toggles all the series but the clicked one
-      const allSeriesLabels = graphSeriesModel.map(series => series.label);
-
-      if (hiddenSeries.length + 1 === allSeriesLabels.length) {
-        this.setState({ hiddenSeries: [] });
-      } else {
-        this.setState({
-          hiddenSeries: difference(allSeriesLabels, [label]),
-        });
-      }
-    }
-  }
-
   render() {
     const { children } = this.props;
-    const { graphSeriesModel, hiddenSeries } = this.state;
-
-    return children({
-      series: graphSeriesModel.map(series => ({
-        ...series,
-        isVisible: hiddenSeries.indexOf(series.label) === -1,
-      })),
-      onSeriesToggle: this.onSeriesToggle,
-      onSeriesColorChange: this.onSeriesColorChange,
-      onSeriesAxisToggle: this.onSeriesAxisToggle,
-      onToggleSort: this.onToggleSort,
-    });
+    const { graphSeriesModel } = this.state;
+
+    return (
+      <GraphSeriesToggler series={graphSeriesModel}>
+        {({ onSeriesToggle, toggledSeries }) => {
+          return children({
+            series: toggledSeries,
+            onSeriesColorChange: this.onSeriesColorChange,
+            onSeriesAxisToggle: this.onSeriesAxisToggle,
+            onToggleSort: this.onToggleSort,
+            onSeriesToggle: onSeriesToggle,
+          });
+        }}
+      </GraphSeriesToggler>
+    );
   }
 }

+ 85 - 0
public/app/plugins/panel/graph2/GraphSeriesToggler.tsx

@@ -0,0 +1,85 @@
+import React from 'react';
+import { GraphSeriesXY } from '@grafana/data';
+import difference from 'lodash/difference';
+import isEqual from 'lodash/isEqual';
+
+interface GraphSeriesTogglerAPI {
+  onSeriesToggle: (label: string, event: React.MouseEvent<HTMLElement>) => void;
+  toggledSeries: GraphSeriesXY[];
+}
+
+interface GraphSeriesTogglerProps {
+  children: (api: GraphSeriesTogglerAPI) => JSX.Element;
+  series: GraphSeriesXY[];
+  onHiddenSeriesChanged?: (hiddenSeries: string[]) => void;
+}
+
+interface GraphSeriesTogglerState {
+  hiddenSeries: string[];
+  toggledSeries: GraphSeriesXY[];
+}
+
+export class GraphSeriesToggler extends React.Component<GraphSeriesTogglerProps, GraphSeriesTogglerState> {
+  constructor(props: GraphSeriesTogglerProps) {
+    super(props);
+
+    this.onSeriesToggle = this.onSeriesToggle.bind(this);
+
+    this.state = {
+      hiddenSeries: [],
+      toggledSeries: props.series,
+    };
+  }
+
+  componentDidUpdate(prevProps: Readonly<GraphSeriesTogglerProps>) {
+    const { series } = this.props;
+    if (!isEqual(prevProps.series, series)) {
+      this.setState({ hiddenSeries: [], toggledSeries: series });
+    }
+  }
+
+  onSeriesToggle(label: string, event: React.MouseEvent<HTMLElement>) {
+    const { series, onHiddenSeriesChanged } = this.props;
+    const { hiddenSeries } = this.state;
+
+    if (event.ctrlKey || event.metaKey || event.shiftKey) {
+      // Toggling series with key makes the series itself to toggle
+      const newHiddenSeries =
+        hiddenSeries.indexOf(label) > -1
+          ? hiddenSeries.filter(series => series !== label)
+          : hiddenSeries.concat([label]);
+
+      const toggledSeries = series.map(series => ({
+        ...series,
+        isVisible: newHiddenSeries.indexOf(series.label) === -1,
+      }));
+      this.setState({ hiddenSeries: newHiddenSeries, toggledSeries }, () =>
+        onHiddenSeriesChanged ? onHiddenSeriesChanged(newHiddenSeries) : undefined
+      );
+      return;
+    }
+
+    // Toggling series with out key toggles all the series but the clicked one
+    const allSeriesLabels = series.map(series => series.label);
+    const newHiddenSeries =
+      hiddenSeries.length + 1 === allSeriesLabels.length ? [] : difference(allSeriesLabels, [label]);
+    const toggledSeries = series.map(series => ({
+      ...series,
+      isVisible: newHiddenSeries.indexOf(series.label) === -1,
+    }));
+
+    this.setState({ hiddenSeries: newHiddenSeries, toggledSeries }, () =>
+      onHiddenSeriesChanged ? onHiddenSeriesChanged(newHiddenSeries) : undefined
+    );
+  }
+
+  render() {
+    const { children } = this.props;
+    const { toggledSeries } = this.state;
+
+    return children({
+      onSeriesToggle: this.onSeriesToggle,
+      toggledSeries,
+    });
+  }
+}

+ 4 - 2
public/app/plugins/panel/graph2/getGraphSeriesModel.ts

@@ -30,7 +30,7 @@ export const getGraphSeriesModel = (
       const field = numberFields[i];
       // Use external calculator just to make sure it works :)
       const points = getFlotPairs({
-        series,
+        rows: series.rows,
         xIndex: timeColumn.index,
         yIndex: field.index,
         nullValueMode: NullValueMode.Null,
@@ -67,7 +67,9 @@ export const getGraphSeriesModel = (
           color: seriesColor,
           info: statsDisplayValues,
           isVisible: true,
-          yAxis: (seriesOptions[field.name] && seriesOptions[field.name].yAxis) || 1,
+          yAxis: {
+            index: (seriesOptions[field.name] && seriesOptions[field.name].yAxis) || 1,
+          },
         });
       }
     }

+ 3 - 1
public/app/plugins/panel/graph2/types.ts

@@ -1,9 +1,11 @@
 import { LegendOptions } from '@grafana/ui';
+import { YAxis } from '@grafana/data';
+
 import { GraphLegendEditorLegendOptions } from './GraphLegendEditor';
 
 export interface SeriesOptions {
   color?: string;
-  yAxis?: number;
+  yAxis?: YAxis;
   [key: string]: any;
 }
 export interface GraphOptions {

+ 2 - 1
public/app/types/explore.ts

@@ -16,6 +16,7 @@ import {
   LogsDedupStrategy,
   LoadingState,
   AbsoluteTimeRange,
+  GraphSeriesXY,
 } from '@grafana/data';
 
 import { Emitter } from 'app/core/core';
@@ -159,7 +160,7 @@ export interface ExploreItemState {
   /**
    * List of timeseries to be shown in the Explore graph result viewer.
    */
-  graphResult?: any[];
+  graphResult?: GraphSeriesXY[];
   /**
    * History of recent queries. Datasource-specific and initialized via localStorage.
    */