From 7dfcd89a04c0c55c4c85cac2b441e02cd5537709 Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Wed, 24 Oct 2018 14:58:24 -0700 Subject: [PATCH] Rework timeSeries in chart viewer (#2987) * rework timeSeries in chart viewer * rework important to fix tests --- .../views/charts/chartInsight.component.ts | 48 +------ .../insights/views/charts/interfaces.ts | 52 ++++++- .../views/charts/types/barChart.component.ts | 4 +- .../views/charts/types/lineChart.component.ts | 3 +- .../charts/types/scatterChart.component.ts | 3 +- .../charts/types/timeSeriesChart.component.ts | 3 +- .../query/editor/charting/chartOptions.ts | 23 ++- .../parts/query/editor/charting/chartView.ts | 20 ++- .../editor/charting/insights/graphInsight.ts | 131 +++++++++++++----- .../query/editor/charting/insights/insight.ts | 9 +- 10 files changed, 202 insertions(+), 94 deletions(-) diff --git a/src/sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component.ts b/src/sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component.ts index d715355353..3bcea19ef2 100644 --- a/src/sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component.ts +++ b/src/sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component.ts @@ -10,10 +10,9 @@ import * as TelemetryUtils from 'sql/common/telemetryUtilities'; import { IInsightsView, IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces'; import { memoize, unmemoize } from 'sql/base/common/decorators'; import { mixin } from 'sql/base/common/objects'; -import { LegendPosition, DataDirection, ChartType } from 'sql/parts/dashboard/widgets/insights/views/charts/interfaces'; +import { LegendPosition, ChartType, defaultChartConfig, IChartConfig, IDataSet, IPointDataSet } from 'sql/parts/dashboard/widgets/insights/views/charts/interfaces'; import * as colors from 'vs/platform/theme/common/colorRegistry'; -import { Color } from 'vs/base/common/color'; import * as types from 'vs/base/common/types'; import { Disposable } from 'vs/base/common/lifecycle'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -22,51 +21,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; declare var Chart: any; -export function customMixin(destination: any, source: any, overwrite?: boolean): any { - if (types.isObject(source)) { - mixin(destination, source, overwrite, customMixin); - } else if (types.isArray(source)) { - for (let i = 0; i < source.length; i++) { - if (destination[i]) { - mixin(destination[i], source[i], overwrite, customMixin); - } else { - destination[i] = source[i]; - } - } - } else { - destination = source; - } - return destination; -} - -export interface IDataSet { - data: Array; - label?: string; -} - -export interface IPointDataSet { - data: Array<{ x: number | string, y: number }>; - label?: string; - fill: boolean; - backgroundColor?: Color; -} - -export interface IChartConfig { - colorMap?: { [column: string]: string }; - labelFirstColumn?: boolean; - legendPosition?: LegendPosition; - dataDirection?: DataDirection; - columnsAsLabels?: boolean; - showTopNData?: number; -} - -export const defaultChartConfig: IChartConfig = { - labelFirstColumn: true, - columnsAsLabels: true, - legendPosition: LegendPosition.Top, - dataDirection: DataDirection.Vertical -}; - @Component({ template: `
; + label?: string; +} + +export interface IPointDataSet { + data: Array<{ x: number | string, y: number }>; + label?: string; + fill: boolean; + backgroundColor?: Color; +} + +export interface IChartConfig { + colorMap?: { [column: string]: string }; + labelFirstColumn?: boolean; + legendPosition?: LegendPosition; + dataDirection?: DataDirection; + columnsAsLabels?: boolean; + showTopNData?: number; +} + +export const defaultChartConfig: IChartConfig = { + labelFirstColumn: true, + columnsAsLabels: true, + legendPosition: LegendPosition.Top, + dataDirection: DataDirection.Vertical +}; diff --git a/src/sql/parts/dashboard/widgets/insights/views/charts/types/barChart.component.ts b/src/sql/parts/dashboard/widgets/insights/views/charts/types/barChart.component.ts index 979cca1c7f..7216581573 100644 --- a/src/sql/parts/dashboard/widgets/insights/views/charts/types/barChart.component.ts +++ b/src/sql/parts/dashboard/widgets/insights/views/charts/types/barChart.component.ts @@ -3,9 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ChartInsight, customMixin, IChartConfig } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component'; +import { ChartInsight } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component'; import { mixin } from 'sql/base/common/objects'; -import { ChartType } from 'sql/parts/dashboard/widgets/insights/views/charts/interfaces'; +import { ChartType, IChartConfig, customMixin } from 'sql/parts/dashboard/widgets/insights/views/charts/interfaces'; import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import * as colors from 'vs/platform/theme/common/colorRegistry'; diff --git a/src/sql/parts/dashboard/widgets/insights/views/charts/types/lineChart.component.ts b/src/sql/parts/dashboard/widgets/insights/views/charts/types/lineChart.component.ts index ed0a7d8d81..6ac32ae81b 100644 --- a/src/sql/parts/dashboard/widgets/insights/views/charts/types/lineChart.component.ts +++ b/src/sql/parts/dashboard/widgets/insights/views/charts/types/lineChart.component.ts @@ -5,11 +5,10 @@ import { mixin } from 'vs/base/common/objects'; -import { defaultChartConfig, IDataSet, IPointDataSet } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component'; import BarChart, { IBarChartConfig } from './barChart.component'; import { memoize, unmemoize } from 'sql/base/common/decorators'; import { clone } from 'sql/base/common/objects'; -import { ChartType, DataType } from 'sql/parts/dashboard/widgets/insights/views/charts/interfaces'; +import { ChartType, DataType, defaultChartConfig, IDataSet, IPointDataSet } from 'sql/parts/dashboard/widgets/insights/views/charts/interfaces'; export interface ILineConfig extends IBarChartConfig { dataType?: DataType; diff --git a/src/sql/parts/dashboard/widgets/insights/views/charts/types/scatterChart.component.ts b/src/sql/parts/dashboard/widgets/insights/views/charts/types/scatterChart.component.ts index 9fbbb64ba3..a5da493af6 100644 --- a/src/sql/parts/dashboard/widgets/insights/views/charts/types/scatterChart.component.ts +++ b/src/sql/parts/dashboard/widgets/insights/views/charts/types/scatterChart.component.ts @@ -3,10 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { defaultChartConfig } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component'; import LineChart, { ILineConfig } from './lineChart.component'; import { clone } from 'sql/base/common/objects'; -import { ChartType } from 'sql/parts/dashboard/widgets/insights/views/charts/interfaces'; +import { ChartType, defaultChartConfig } from 'sql/parts/dashboard/widgets/insights/views/charts/interfaces'; import { mixin } from 'vs/base/common/objects'; diff --git a/src/sql/parts/dashboard/widgets/insights/views/charts/types/timeSeriesChart.component.ts b/src/sql/parts/dashboard/widgets/insights/views/charts/types/timeSeriesChart.component.ts index 646cf6b11f..a9e1ce9d72 100644 --- a/src/sql/parts/dashboard/widgets/insights/views/charts/types/timeSeriesChart.component.ts +++ b/src/sql/parts/dashboard/widgets/insights/views/charts/types/timeSeriesChart.component.ts @@ -3,10 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { defaultChartConfig, IPointDataSet } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component'; import LineChart, { ILineConfig } from './lineChart.component'; import { clone } from 'sql/base/common/objects'; -import { ChartType } from 'sql/parts/dashboard/widgets/insights/views/charts/interfaces'; +import { ChartType, defaultChartConfig, IPointDataSet } from 'sql/parts/dashboard/widgets/insights/views/charts/interfaces'; import { mixin } from 'vs/base/common/objects'; import { Color } from 'vs/base/common/color'; diff --git a/src/sql/parts/query/editor/charting/chartOptions.ts b/src/sql/parts/query/editor/charting/chartOptions.ts index fed3c591f6..104b6b4f5f 100644 --- a/src/sql/parts/query/editor/charting/chartOptions.ts +++ b/src/sql/parts/query/editor/charting/chartOptions.ts @@ -18,7 +18,8 @@ export enum ControlType { combo, numberInput, input, - checkbox + checkbox, + dateInput } export interface IChartOption { @@ -115,6 +116,20 @@ const xAxisMaxInput: IChartOption = { default: undefined }; +const xAxisMinDateInput: IChartOption = { + label: localize('xAxisMinDate', 'X Axis Minimum Date'), + type: ControlType.dateInput, + configEntry: 'xAxisMin', + default: undefined +}; + +const xAxisMaxDateInput: IChartOption = { + label: localize('xAxisMaxDate', 'X Axis Maximum Date'), + type: ControlType.dateInput, + configEntry: 'xAxisMax', + default: undefined +}; + const dataTypeInput: IChartOption = { label: localize('dataTypeLabel', 'Data Type'), type: ControlType.combo, @@ -150,7 +165,11 @@ export const ChartOptions: IChartOptions = { [ChartType.TimeSeries]: [ legendInput, yAxisLabelInput, - xAxisLabelInput + yAxisMinInput, + yAxisMaxInput, + xAxisLabelInput, + xAxisMinDateInput, + xAxisMaxDateInput, ], [ChartType.Bar]: [ dataDirectionOption, diff --git a/src/sql/parts/query/editor/charting/chartView.ts b/src/sql/parts/query/editor/charting/chartView.ts index 09217cf8b7..c96ea26bbb 100644 --- a/src/sql/parts/query/editor/charting/chartView.ts +++ b/src/sql/parts/query/editor/charting/chartView.ts @@ -275,7 +275,7 @@ export class ChartView implements IPanelView { dropdown.render(optionContainer); dropdown.onDidSelect(e => { if (this.options[option.configEntry] !== option.options[e.index]) { - this.options[option.configEntry] = option.options[e.index] === 'timeSeries' ? 'line' : option.options[e.index]; + this.options[option.configEntry] = option.options[e.index]; if (this.insight) { this.insight.options = this.options; } @@ -324,6 +324,24 @@ export class ChartView implements IPanelView { }; this.optionDisposables.push(attachInputBoxStyler(numberInput, this._themeService)); break; + case ControlType.dateInput: + let dateInput = new InputBox(optionContainer, this._contextViewService, { type: 'date' }); + dateInput.value = value || ''; + dateInput.onDidChange(e => { + if (this.options[option.configEntry] !== e) { + this.options[option.configEntry] = e; + if (this.insight) { + this.insight.options = this.options; + } + } + }); + setFunc = (val: string) => { + if (!isUndefinedOrNull(val)) { + dateInput.value = val; + } + }; + this.optionDisposables.push(attachInputBoxStyler(dateInput, this._themeService)); + break; } this.optionMap[option.configEntry] = { element: optionContainer, set: setFunc }; container.appendChild(optionContainer); diff --git a/src/sql/parts/query/editor/charting/insights/graphInsight.ts b/src/sql/parts/query/editor/charting/insights/graphInsight.ts index cc1a61735f..b73ece6a38 100644 --- a/src/sql/parts/query/editor/charting/insights/graphInsight.ts +++ b/src/sql/parts/query/editor/charting/insights/graphInsight.ts @@ -7,7 +7,7 @@ import { Chart as ChartJs } from 'chart.js'; -import { mixin } from 'vs/base/common/objects'; +import { mixin } from 'sql/base/common/objects'; import { localize } from 'vs/nls'; import * as colors from 'vs/platform/theme/common/colorRegistry'; import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry'; @@ -15,10 +15,28 @@ import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces'; import { IInsightOptions, IInsight } from './interfaces'; -import { ChartType, DataDirection, LegendPosition } from 'sql/parts/dashboard/widgets/insights/views/charts/interfaces'; +import { ChartType, DataDirection, LegendPosition, DataType, IPointDataSet, customMixin } from 'sql/parts/dashboard/widgets/insights/views/charts/interfaces'; const noneLineGraphs = [ChartType.Doughnut, ChartType.Pie]; +const timeSeriesScales = { + scales: { + xAxes: [{ + type: 'time', + display: true, + ticks: { + autoSkip: false, + maxRotation: 45, + minRotation: 45 + } + }], + + yAxes: [{ + display: true, + }] + } +}; + const defaultOptions: IInsightOptions = { type: ChartType.Bar, dataDirection: DataDirection.Horizontal @@ -30,6 +48,8 @@ export class Graph implements IInsight { private chartjs: ChartJs; private _data: IInsightData; + private originalType: ChartType; + public static readonly types = [ChartType.Bar, ChartType.Doughnut, ChartType.HorizontalBar, ChartType.Line, ChartType.Pie, ChartType.Scatter, ChartType.TimeSeries]; public readonly types = Graph.types; @@ -83,37 +103,51 @@ export class Graph implements IInsight { labels = data.rows.map(row => row[0]); } - if (this.options.dataDirection === DataDirection.Horizontal) { - if (this.options.labelFirstColumn) { - chartData = data.rows.map((row) => { - return { - data: row.map(item => Number(item)).slice(1), - label: row[0] - }; - }); - } else { - chartData = data.rows.map((row, i) => { - return { - data: row.map(item => Number(item)), - label: localize('series', 'Series {0}', i) - }; - }); - } + if (this.originalType === ChartType.TimeSeries) { + let dataSetMap: { [label: string]: IPointDataSet } = {}; + this._data.rows.map(row => { + if (row && row.length >= 3) { + let legend = row[0]; + if (!dataSetMap[legend]) { + dataSetMap[legend] = { label: legend, data: [], fill: false }; + } + dataSetMap[legend].data.push({ x: row[1], y: Number(row[2]) }); + } + }); + chartData = Object.values(dataSetMap); } else { - if (this.options.columnsAsLabels) { - chartData = data.rows[0].slice(1).map((row, i) => { - return { - data: data.rows.map(row => Number(row[i + 1])), - label: data.columns[i + 1] - }; - }); + if (this.options.dataDirection === DataDirection.Horizontal) { + if (this.options.labelFirstColumn) { + chartData = data.rows.map((row) => { + return { + data: row.map(item => Number(item)).slice(1), + label: row[0] + }; + }); + } else { + chartData = data.rows.map((row, i) => { + return { + data: row.map(item => Number(item)), + label: localize('series', 'Series {0}', i) + }; + }); + } } else { - chartData = data.rows[0].slice(1).map((row, i) => { - return { - data: data.rows.map(row => Number(row[i + 1])), - label: localize('series', 'Series {0}', i + 1) - }; - }); + if (this.options.columnsAsLabels) { + chartData = data.rows[0].slice(1).map((row, i) => { + return { + data: data.rows.map(row => Number(row[i + 1])), + label: data.columns[i + 1] + }; + }); + } else { + chartData = data.rows[0].slice(1).map((row, i) => { + return { + data: data.rows.map(row => Number(row[i + 1])), + label: localize('series', 'Series {0}', i + 1) + }; + }); + } } } @@ -187,6 +221,35 @@ export class Graph implements IInsight { color: gridLines } }]; + + if (this.originalType === ChartType.TimeSeries) { + retval = mixin(retval, timeSeriesScales, true, customMixin); + if (options.xAxisMax) { + retval = mixin(retval, { + scales: { + xAxes: [{ + type: 'time', + time: { + max: options.xAxisMax + } + }], + } + }, true, customMixin); + } + + if (options.xAxisMin) { + retval = mixin(retval, { + scales: { + xAxes: [{ + type: 'time', + time: { + min: options.xAxisMin + } + }], + } + }, true, customMixin); + } + } } retval.legend = { @@ -208,6 +271,12 @@ export class Graph implements IInsight { public set options(options: IInsightOptions) { this._options = options; + this.originalType = options.type as ChartType; + if (this.options.type === ChartType.TimeSeries) { + this.options.type = ChartType.Line; + this.options.dataType = DataType.Point; + this.options.dataDirection = DataDirection.Horizontal; + } this.data = this._data; } diff --git a/src/sql/parts/query/editor/charting/insights/insight.ts b/src/sql/parts/query/editor/charting/insights/insight.ts index 926f7a87d7..3c3ce55f8b 100644 --- a/src/sql/parts/query/editor/charting/insights/insight.ts +++ b/src/sql/parts/query/editor/charting/insights/insight.ts @@ -7,7 +7,7 @@ import { Graph } from './graphInsight'; import { IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces'; -import { DataDirection, ChartType } from 'sql/parts/dashboard/widgets/insights/views/charts/interfaces'; +import { DataDirection, ChartType, DataType } from 'sql/parts/dashboard/widgets/insights/views/charts/interfaces'; import { ImageInsight } from './imageInsight'; import { TableInsight } from './tableInsight'; import { IInsightOptions, IInsight, InsightType, IInsightCtor } from './interfaces'; @@ -16,6 +16,7 @@ import { CountInsight } from './countInsight'; import { Builder } from 'vs/base/browser/builder'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Dimension } from 'vs/base/browser/dom'; +import { deepClone } from 'vs/base/common/objects'; const defaultOptions: IInsightOptions = { type: ChartType.Bar, @@ -47,13 +48,13 @@ export class Insight { } public set options(val: IInsightOptions) { - this._options = val; + this._options = deepClone(val); if (this.insight) { // check to see if we need to change the insight type - if (!this.insight.types.includes(val.type)) { + if (!this.insight.types.includes(this.options.type)) { this.buildInsight(); } else { - this.insight.options = val; + this.insight.options = this.options; } } }