Rework timeSeries in chart viewer (#2987)

* rework timeSeries in chart viewer

* rework important to fix tests
This commit is contained in:
Anthony Dresser
2018-10-24 14:58:24 -07:00
committed by Karl Burtram
parent cb224fbc74
commit 5d63905056
10 changed files with 202 additions and 94 deletions

View File

@@ -10,10 +10,9 @@ import * as TelemetryUtils from 'sql/common/telemetryUtilities';
import { IInsightsView, IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces'; import { IInsightsView, IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces';
import { memoize, unmemoize } from 'sql/base/common/decorators'; import { memoize, unmemoize } from 'sql/base/common/decorators';
import { mixin } from 'sql/base/common/objects'; 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 * as colors from 'vs/platform/theme/common/colorRegistry';
import { Color } from 'vs/base/common/color';
import * as types from 'vs/base/common/types'; import * as types from 'vs/base/common/types';
import { Disposable } from 'vs/base/common/lifecycle'; import { Disposable } from 'vs/base/common/lifecycle';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; 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; 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<number>;
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({ @Component({
template: ` <div style="display: block; width: 100%; height: 100%; position: relative"> template: ` <div style="display: block; width: 100%; height: 100%; position: relative">
<canvas #canvas *ngIf="_isDataAvailable && _hasInit" <canvas #canvas *ngIf="_isDataAvailable && _hasInit"

View File

@@ -3,6 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { Color } from 'vs/base/common/color';
import * as types from 'vs/base/common/types';
import { mixin } from 'sql/base/common/objects';
export enum ChartType { export enum ChartType {
Bar = 'bar', Bar = 'bar',
Doughnut = 'doughnut', Doughnut = 'doughnut',
@@ -30,3 +35,48 @@ export enum DataType {
Number = 'number', Number = 'number',
Point = 'point' Point = 'point'
} }
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<number>;
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
};

View File

@@ -3,9 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * 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 { 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 { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as colors from 'vs/platform/theme/common/colorRegistry'; import * as colors from 'vs/platform/theme/common/colorRegistry';

View File

@@ -5,11 +5,10 @@
import { mixin } from 'vs/base/common/objects'; 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 BarChart, { IBarChartConfig } from './barChart.component';
import { memoize, unmemoize } from 'sql/base/common/decorators'; import { memoize, unmemoize } from 'sql/base/common/decorators';
import { clone } from 'sql/base/common/objects'; 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 { export interface ILineConfig extends IBarChartConfig {
dataType?: DataType; dataType?: DataType;

View File

@@ -3,10 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * 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 LineChart, { ILineConfig } from './lineChart.component';
import { clone } from 'sql/base/common/objects'; 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'; import { mixin } from 'vs/base/common/objects';

View File

@@ -3,10 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * 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 LineChart, { ILineConfig } from './lineChart.component';
import { clone } from 'sql/base/common/objects'; 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 { mixin } from 'vs/base/common/objects';
import { Color } from 'vs/base/common/color'; import { Color } from 'vs/base/common/color';

View File

@@ -18,7 +18,8 @@ export enum ControlType {
combo, combo,
numberInput, numberInput,
input, input,
checkbox checkbox,
dateInput
} }
export interface IChartOption { export interface IChartOption {
@@ -115,6 +116,20 @@ const xAxisMaxInput: IChartOption = {
default: undefined 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 = { const dataTypeInput: IChartOption = {
label: localize('dataTypeLabel', 'Data Type'), label: localize('dataTypeLabel', 'Data Type'),
type: ControlType.combo, type: ControlType.combo,
@@ -150,7 +165,11 @@ export const ChartOptions: IChartOptions = {
[ChartType.TimeSeries]: [ [ChartType.TimeSeries]: [
legendInput, legendInput,
yAxisLabelInput, yAxisLabelInput,
xAxisLabelInput yAxisMinInput,
yAxisMaxInput,
xAxisLabelInput,
xAxisMinDateInput,
xAxisMaxDateInput,
], ],
[ChartType.Bar]: [ [ChartType.Bar]: [
dataDirectionOption, dataDirectionOption,

View File

@@ -276,7 +276,7 @@ export class ChartView extends Disposable implements IPanelView {
dropdown.render(optionContainer); dropdown.render(optionContainer);
dropdown.onDidSelect(e => { dropdown.onDidSelect(e => {
if (this.options[option.configEntry] !== option.options[e.index]) { 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) { if (this.insight) {
this.insight.options = this.options; this.insight.options = this.options;
} }
@@ -325,6 +325,24 @@ export class ChartView extends Disposable implements IPanelView {
}; };
this.optionDisposables.push(attachInputBoxStyler(numberInput, this._themeService)); this.optionDisposables.push(attachInputBoxStyler(numberInput, this._themeService));
break; 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 }; this.optionMap[option.configEntry] = { element: optionContainer, set: setFunc };
container.appendChild(optionContainer); container.appendChild(optionContainer);

View File

@@ -7,7 +7,7 @@
import { Chart as ChartJs } from 'chart.js'; 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 { localize } from 'vs/nls';
import * as colors from 'vs/platform/theme/common/colorRegistry'; import * as colors from 'vs/platform/theme/common/colorRegistry';
import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry'; 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 { IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces';
import { IInsightOptions, IInsight } from './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 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 = { const defaultOptions: IInsightOptions = {
type: ChartType.Bar, type: ChartType.Bar,
dataDirection: DataDirection.Horizontal dataDirection: DataDirection.Horizontal
@@ -30,6 +48,8 @@ export class Graph implements IInsight {
private chartjs: ChartJs; private chartjs: ChartJs;
private _data: IInsightData; 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 static readonly types = [ChartType.Bar, ChartType.Doughnut, ChartType.HorizontalBar, ChartType.Line, ChartType.Pie, ChartType.Scatter, ChartType.TimeSeries];
public readonly types = Graph.types; public readonly types = Graph.types;
@@ -83,37 +103,51 @@ export class Graph implements IInsight {
labels = data.rows.map(row => row[0]); labels = data.rows.map(row => row[0]);
} }
if (this.options.dataDirection === DataDirection.Horizontal) { if (this.originalType === ChartType.TimeSeries) {
if (this.options.labelFirstColumn) { let dataSetMap: { [label: string]: IPointDataSet } = {};
chartData = data.rows.map((row) => { this._data.rows.map(row => {
return { if (row && row.length >= 3) {
data: row.map(item => Number(item)).slice(1), let legend = row[0];
label: row[0] if (!dataSetMap[legend]) {
}; dataSetMap[legend] = { label: legend, data: [], fill: false };
}); }
} else { dataSetMap[legend].data.push({ x: row[1], y: Number(row[2]) });
chartData = data.rows.map((row, i) => { }
return { });
data: row.map(item => Number(item)), chartData = Object.values(dataSetMap);
label: localize('series', 'Series {0}', i)
};
});
}
} else { } else {
if (this.options.columnsAsLabels) { if (this.options.dataDirection === DataDirection.Horizontal) {
chartData = data.rows[0].slice(1).map((row, i) => { if (this.options.labelFirstColumn) {
return { chartData = data.rows.map((row) => {
data: data.rows.map(row => Number(row[i + 1])), return {
label: data.columns[i + 1] 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 { } else {
chartData = data.rows[0].slice(1).map((row, i) => { if (this.options.columnsAsLabels) {
return { chartData = data.rows[0].slice(1).map((row, i) => {
data: data.rows.map(row => Number(row[i + 1])), return {
label: localize('series', 'Series {0}', i + 1) 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 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 = <ChartJs.ChartLegendOptions>{ retval.legend = <ChartJs.ChartLegendOptions>{
@@ -208,6 +271,12 @@ export class Graph implements IInsight {
public set options(options: IInsightOptions) { public set options(options: IInsightOptions) {
this._options = options; 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; this.data = this._data;
} }

View File

@@ -7,7 +7,7 @@
import { Graph } from './graphInsight'; import { Graph } from './graphInsight';
import { IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces'; 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 { ImageInsight } from './imageInsight';
import { TableInsight } from './tableInsight'; import { TableInsight } from './tableInsight';
import { IInsightOptions, IInsight, InsightType, IInsightCtor } from './interfaces'; import { IInsightOptions, IInsight, InsightType, IInsightCtor } from './interfaces';
@@ -16,6 +16,7 @@ import { CountInsight } from './countInsight';
import { Builder } from 'vs/base/browser/builder'; import { Builder } from 'vs/base/browser/builder';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Dimension } from 'vs/base/browser/dom'; import { Dimension } from 'vs/base/browser/dom';
import { deepClone } from 'vs/base/common/objects';
const defaultOptions: IInsightOptions = { const defaultOptions: IInsightOptions = {
type: ChartType.Bar, type: ChartType.Bar,
@@ -47,13 +48,13 @@ export class Insight {
} }
public set options(val: IInsightOptions) { public set options(val: IInsightOptions) {
this._options = val; this._options = deepClone(val);
if (this.insight) { if (this.insight) {
// check to see if we need to change the insight type // 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(); this.buildInsight();
} else { } else {
this.insight.options = val; this.insight.options = this.options;
} }
} }
} }