Refactor chart viewer (#2381)

* working on adding charts

* working on chart options

* adding image and table insight

* add chart viewing and handle a bunch of small bugs

* formatting

* remove unused code
This commit is contained in:
Anthony Dresser
2018-08-31 12:55:34 -07:00
committed by GitHub
parent 54ee1c23f0
commit 8e0c19fc8d
25 changed files with 1919 additions and 40 deletions

View File

@@ -142,3 +142,8 @@ panel {
.visibility.hidden {
visibility: hidden;
}
.tabBody {
width: 100%;
height: 100%;
}

View File

@@ -78,12 +78,16 @@ export class TabbedPanel extends Disposable implements IThemable {
} else {
this._headerVisible = false;
}
this.$body = $('tabBody');
this.$body = $('.tabBody');
this.$body.attr('role', 'tabpanel');
this.$body.attr('tabindex', '0');
this.$parent.append(this.$body);
}
public contains(tab: IPanelTab): boolean {
return this._tabMap.has(tab.identifier);
}
public pushTab(tab: IPanelTab): PanelTabIdentifier {
let internalTab = tab as IInternalPanelTab;
this._tabMap.set(tab.identifier, internalTab);
@@ -94,6 +98,7 @@ export class TabbedPanel extends Disposable implements IThemable {
if (this._tabMap.size > 1 && !this._headerVisible) {
this.$parent.append(this.$header, 0);
this._headerVisible = true;
this.layout(this._currentDimensions);
}
return tab.identifier as PanelTabIdentifier;
}
@@ -170,6 +175,8 @@ export class TabbedPanel extends Disposable implements IThemable {
public layout(dimension: Dimension): void {
this._currentDimensions = dimension;
this.$parent.style('height', dimension.height + 'px');
this.$parent.style('width', dimension.width + 'px');
this.$header.style('width', dimension.width + 'px');
this.$body.style('width', dimension.width + 'px');
const bodyHeight = dimension.height - (this._headerVisible ? this.headersize : 0);

View File

@@ -103,8 +103,8 @@ export const defaultChartConfig: IChartConfig = {
})
export abstract class ChartInsight extends Disposable implements IInsightsView {
private _isDataAvailable: boolean = false;
private _hasInit: boolean = false;
private _hasError: boolean = false;
protected _hasInit: boolean = false;
protected _hasError: boolean = false;
private _options: any = {};
@ViewChild(BaseChartDirective) private _chart: BaseChartDirective;
@@ -113,7 +113,7 @@ export abstract class ChartInsight extends Disposable implements IInsightsView {
protected _config: IChartConfig;
protected _data: IInsightData;
private readonly CHART_ERROR_MESSAGE = nls.localize('chartErrorMessage', 'Chart cannot be displayed with the given data');
protected readonly CHART_ERROR_MESSAGE = nls.localize('chartErrorMessage', 'Chart cannot be displayed with the given data');
protected abstract get chartType(): ChartType;

View File

@@ -5,9 +5,9 @@
import { defaultChartConfig, IPointDataSet, ChartType } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import LineChart, { ILineConfig } from './lineChart.component';
import { clone } from 'sql/base/common/objects';
import { mixin } from 'vs/base/common/objects';
import { clone } from 'sql/base/common/objects';
import { Color } from 'vs/base/common/color';
const defaultTimeSeriesConfig = mixin(clone(defaultChartConfig), { dataType: 'point', dataDirection: 'horizontal' }) as ILineConfig;

View File

@@ -15,8 +15,8 @@ import { IInsightsView, IInsightData } from 'sql/parts/dashboard/widgets/insight
`
})
export default class CountInsight implements IInsightsView {
private _labels: Array<string>;
private _values: Array<string>;
protected _labels: Array<string>;
protected _values: Array<string>;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef) { }

View File

@@ -250,8 +250,7 @@
}
.grid-panel .action-label.icon {
height: 35px;
line-height: 35px;
height: 16px;
min-width: 28px;
background-size: 16px;
background-position: center center;

View File

@@ -73,9 +73,6 @@
-->
<ng-template #lineInput>
<ng-container *ngTemplateOutlet="dataTypeInput"></ng-container>
<ng-template [ngIf]="showDataDirection">
<ng-container *ngTemplateOutlet="dataDirectionInput"></ng-container>
</ng-template>
<ng-template [ngIf]="showColumnsAsLabels">
<ng-container *ngTemplateOutlet="columnsAsLabelsInput"></ng-container>
</ng-template>

View File

@@ -16,6 +16,11 @@ import QueryRunner from 'sql/parts/query/execution/queryRunner';
import { SaveFormat } from 'sql/parts/grid/common/interfaces';
import { Table } from 'sql/base/browser/ui/table/table';
import { GridTableState } from 'sql/parts/query/editor/gridPanel';
import { IEditorService } from 'vs/platform/editor/common/editor';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { QueryEditor } from './queryEditor';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
export interface IGridActionContext {
cell: { row: number; cell: number; };
@@ -158,3 +163,23 @@ export class MinimizeTableAction extends Action {
return TPromise.as(true);
}
}
export class ChartDataAction extends Action {
public static ID = 'grid.chart';
public static LABEL = localize('chart', 'Chart');
public static ICON = 'viewChart';
constructor(@IWorkbenchEditorService private editorService: IWorkbenchEditorService) {
super(ChartDataAction.ID, ChartDataAction.LABEL, ChartDataAction.ICON);
}
public run(context: IGridActionContext): TPromise<boolean> {
let activeEditor = this.editorService.getActiveEditor();
if (activeEditor instanceof QueryEditor) {
activeEditor.resultsEditor.chart({ batchId: context.batchId, resultId: context.resultId });
return TPromise.as(true);
} else {
return TPromise.as(false);
}
}
}

View File

@@ -0,0 +1,204 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { localize } from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
import { ChartType, DataDirection, LegendPosition } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
import { DataType } from 'sql/parts/dashboard/widgets/insights/views/charts/types/lineChart.component';
import { InsightType, IInsightOptions } from './insights/interfaces';
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
export enum ControlType {
combo,
numberInput,
input,
checkbox
}
export interface IChartOption {
label: string;
type: ControlType;
configEntry: string;
default: any;
options?: any[];
displayableOptions?: string[];
if?: (options: IInsightOptions) => boolean;
}
export interface IChartOptions {
general: Array<IChartOption>;
[x: string]: Array<IChartOption>;
}
const dataDirectionOption: IChartOption = {
label: localize('dataDirectionLabel', 'Data Direction'),
type: ControlType.combo,
displayableOptions: [localize('verticalLabel', 'Vertical'), localize('horizontalLabel', 'Horizontal')],
options: [DataDirection.Vertical, DataDirection.Horizontal],
configEntry: 'dataDirection',
default: DataDirection.Horizontal
};
const columnsAsLabelsInput: IChartOption = {
label: localize('columnsAsLabelsLabel', 'Use column names as labels'),
type: ControlType.checkbox,
configEntry: 'columnsAsLabels',
default: false,
if: (options: IInsightOptions) => {
return options.dataDirection === DataDirection.Vertical && options.dataType !== DataType.Point;
}
};
const labelFirstColumnInput: IChartOption = {
label: localize('labelFirstColumnLabel', 'Use first column as row label'),
type: ControlType.checkbox,
configEntry: 'labelFirstColumn',
default: false,
if: (options: IInsightOptions) => {
return options.dataDirection === DataDirection.Horizontal && options.dataType !== DataType.Point;
}
};
const legendInput: IChartOption = {
label: localize('legendLabel', 'Legend Position'),
type: ControlType.combo,
options: Object.values(LegendPosition),
configEntry: 'legendPosition',
default: LegendPosition.Top
};
const yAxisLabelInput: IChartOption = {
label: localize('yAxisLabel', 'Y Axis Label'),
type: ControlType.input,
configEntry: 'yAxisLabel',
default: undefined
};
const yAxisMinInput: IChartOption = {
label: localize('yAxisMinVal', 'Y Axis Minimum Value'),
type: ControlType.numberInput,
configEntry: 'yAxisMin',
default: undefined
};
const yAxisMaxInput: IChartOption = {
label: localize('yAxisMaxVal', 'Y Axis Maximum Value'),
type: ControlType.numberInput,
configEntry: 'yAxisMax',
default: undefined
};
const xAxisLabelInput: IChartOption = {
label: localize('xAxisLabel', 'X Axis Label'),
type: ControlType.input,
configEntry: 'xAxisLabel',
default: undefined
};
const xAxisMinInput: IChartOption = {
label: localize('xAxisMinVal', 'X Axis Minimum Value'),
type: ControlType.numberInput,
configEntry: 'xAxisMin',
default: undefined
};
const xAxisMaxInput: IChartOption = {
label: localize('xAxisMaxVal', 'X Axis Maximum Value'),
type: ControlType.numberInput,
configEntry: 'xAxisMax',
default: undefined
};
const dataTypeInput: IChartOption = {
label: localize('dataTypeLabel', 'Data Type'),
type: ControlType.combo,
options: [DataType.Number, DataType.Point],
displayableOptions: [localize('numberLabel', 'Number'), localize('pointLabel', 'Point')],
configEntry: 'dataType',
default: DataType.Number
};
export const ChartOptions: IChartOptions = {
general: [
{
label: localize('chartTypeLabel', 'Chart Type'),
type: ControlType.combo,
options: insightRegistry.getAllIds(),
configEntry: 'type',
default: ChartType.Bar
}
],
[ChartType.Line]: [
dataTypeInput,
columnsAsLabelsInput,
labelFirstColumnInput,
yAxisLabelInput,
xAxisLabelInput,
legendInput
],
[ChartType.Scatter]: [
legendInput,
yAxisLabelInput,
xAxisLabelInput
],
[ChartType.TimeSeries]: [
legendInput,
yAxisLabelInput,
xAxisLabelInput
],
[ChartType.Bar]: [
dataDirectionOption,
columnsAsLabelsInput,
labelFirstColumnInput,
legendInput,
yAxisLabelInput,
yAxisMinInput,
yAxisMaxInput,
xAxisLabelInput
],
[ChartType.HorizontalBar]: [
dataDirectionOption,
columnsAsLabelsInput,
labelFirstColumnInput,
legendInput,
xAxisLabelInput,
xAxisMinInput,
xAxisMaxInput,
yAxisLabelInput
],
[ChartType.Pie]: [
dataDirectionOption,
columnsAsLabelsInput,
labelFirstColumnInput,
legendInput
],
[ChartType.Doughnut]: [
dataDirectionOption,
columnsAsLabelsInput,
labelFirstColumnInput,
legendInput
],
[InsightType.Table]: [],
[InsightType.Count]: [],
[InsightType.Image]: [
{
configEntry: 'encoding',
label: localize('encodingOption', 'Encoding'),
type: ControlType.input,
default: 'hex'
},
{
configEntry: 'imageFormat',
label: localize('imageFormatOption', 'Image Format'),
type: ControlType.input,
default: 'jpeg'
}
]
};

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IPanelTab } from 'sql/base/browser/ui/panel/panel';
import { ChartView } from './chartView';
import QueryRunner from 'sql/parts/query/execution/queryRunner';
import { localize } from 'vs/nls';
import { generateUuid } from 'vs/base/common/uuid';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export class ChartTab implements IPanelTab {
public readonly title = localize('chartTabTitle', 'Chart');
public readonly identifier = generateUuid();
public readonly view: ChartView;
constructor(@IInstantiationService instantiationService: IInstantiationService) {
this.view = instantiationService.createInstance(ChartView);
}
public set queryRunner(runner: QueryRunner) {
this.view.queryRunner = runner;
}
public chart(dataId: { batchId: number, resultId: number}): void {
this.view.chart(dataId);
}
}

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.chart-view-container {
display: flex;
width: 100%;
height: 100%;
}
.graph-container {
flex: 1 1 0;
}
.options-container {
width: 250px;
padding-right: 10px;
}

View File

@@ -0,0 +1,240 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./chartView';
import { IPanelView } from 'sql/base/browser/ui/panel/panel';
import { Insight } from './insights/insight';
import QueryRunner from 'sql/parts/query/execution/queryRunner';
import { IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces';
import { ChartOptions, IChartOption, ControlType } from './chartOptions';
import { ChartType } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { IInsightOptions } from './insights/interfaces';
import { Dimension, $, getContentHeight, getContentWidth } from 'vs/base/browser/dom';
import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { Builder } from 'vs/base/browser/builder';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { attachSelectBoxStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
declare class Proxy {
constructor(object, handler);
}
export class ChartView implements IPanelView {
private insight: Insight;
private _queryRunner: QueryRunner;
private _data: IInsightData;
private _currentData: { batchId: number, resultId: number };
private optionsControl: HTMLElement;
private options: IInsightOptions = {
type: ChartType.Bar
};
private container: HTMLElement;
private typeControls: HTMLElement;
private graphContainer: HTMLElement;
private optionDisposables: IDisposable[] = [];
private optionMap: { [x: string]: HTMLElement } = {};
constructor(
@IContextViewService private _contextViewService: IContextViewService,
@IThemeService private _themeService: IThemeService,
@IInstantiationService private _instantiationService: IInstantiationService
) {
this.optionsControl = $('div.options-container');
let generalControls = $('div.general-controls');
this.optionsControl.appendChild(generalControls);
this.typeControls = $('div.type-controls');
this.optionsControl.appendChild(this.typeControls);
let self = this;
this.options = new Proxy(this.options, {
get: function (target, key, receiver) {
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
let change = false;
if (target[key] !== value) {
change = true;
}
let result = Reflect.set(target, key, value, receiver);
if (change) {
if (key === 'type') {
self.buildOptions();
} else {
self.verifyOptions();
}
}
return result;
}
}) as IInsightOptions;
ChartOptions.general.map(o => {
this.createOption(o, generalControls);
});
}
render(container: HTMLElement): void {
if (!this.container) {
this.container = $('div.chart-view-container');
this.graphContainer = $('div.graph-container');
this.container.appendChild(this.graphContainer);
this.container.appendChild(this.optionsControl);
this.insight = new Insight(this.graphContainer, this.options, this._instantiationService);
}
container.appendChild(this.container);
if (this._data) {
this.insight.data = this._data;
} else {
this.queryRunner = this._queryRunner;
}
}
public chart(dataId: { batchId: number, resultId: number }) {
this._currentData = dataId;
this.shouldGraph();
}
layout(dimension: Dimension): void {
if (this.insight) {
this.insight.layout(new Dimension(getContentWidth(this.graphContainer), getContentHeight(this.graphContainer)));
}
}
public set queryRunner(runner: QueryRunner) {
this._queryRunner = runner;
this.shouldGraph();
}
private shouldGraph() {
// Check if we have the necessary information
if (this._currentData && this._queryRunner) {
// check if we are being asked to graph something that is available
let batch = this._queryRunner.batchSets[this._currentData.batchId];
if (batch) {
let summary = batch.resultSetSummaries[this._currentData.resultId];
if (summary) {
this._queryRunner.getQueryRows(0, summary.rowCount, 0, 0).then(d => {
this._data = {
columns: summary.columnInfo.map(c => c.columnName),
rows: d.resultSubset.rows.map(r => r.map(c => c.displayValue))
};
if (this.insight) {
this.insight.data = this._data;
}
});
}
}
// if we have the necessary information but the information isn't avaiable yet,
// we should be smart and retrying when the information might be available
}
}
private buildOptions() {
dispose(this.optionDisposables);
this.optionDisposables = [];
this.optionMap = {};
new Builder(this.typeControls).clearChildren();
ChartOptions[this.options.type].map(o => {
this.createOption(o, this.typeControls);
});
if (this.insight) {
this.insight.options = this.options;
}
this.verifyOptions();
}
private verifyOptions() {
for (let key in this.optionMap) {
if (this.optionMap.hasOwnProperty(key)) {
let option = ChartOptions[this.options.type].find(e => e.configEntry === key);
if (option && option.if) {
if (option.if(this.options)) {
new Builder(this.optionMap[key]).show();
} else {
new Builder(this.optionMap[key]).hide();
}
}
}
}
}
private createOption(option: IChartOption, container: HTMLElement) {
let label = $('div');
label.innerText = option.label;
let optionContainer = $('div.option-container');
optionContainer.appendChild(label);
switch (option.type) {
case ControlType.checkbox:
let checkbox = new Checkbox(optionContainer, {
label: '',
ariaLabel: option.label,
checked: option.default,
onChange: () => {
if (this.options[option.configEntry] !== checkbox.checked) {
this.options[option.configEntry] = checkbox.checked;
this.insight.options = this.options;
}
}
});
break;
case ControlType.combo:
let dropdown = new SelectBox(option.displayableOptions || option.options, 0, this._contextViewService);
dropdown.select(option.options.indexOf(option.default));
dropdown.render(optionContainer);
dropdown.onDidSelect(e => {
if (this.options[option.configEntry] !== option.options[e.index]) {
this.options[option.configEntry] = option.options[e.index];
this.insight.options = this.options;
}
});
this.optionDisposables.push(attachSelectBoxStyler(dropdown, this._themeService));
break;
case ControlType.input:
let input = new InputBox(optionContainer, this._contextViewService);
input.value = option.default || '';
input.onDidChange(e => {
if (this.options[option.configEntry] !== e) {
this.options[option.configEntry] = e;
this.insight.options = this.options;
}
});
this.optionDisposables.push(attachInputBoxStyler(input, this._themeService));
break;
case ControlType.numberInput:
let numberInput = new InputBox(optionContainer, this._contextViewService, { type: 'number' });
numberInput.value = option.default || '';
numberInput.onDidChange(e => {
if (this.options[option.configEntry] !== Number(e)) {
this.options[option.configEntry] = Number(e);
this.insight.options = this.options;
}
});
this.optionDisposables.push(attachInputBoxStyler(numberInput, this._themeService));
break;
}
this.optionMap[option.configEntry] = optionContainer;
container.appendChild(optionContainer);
this.options[option.configEntry] = option.default;
}
}

View File

@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.count-label-container {
margin-left: 5px;
}
.label-container {
font-size: 20px
}
.value-container {
}

View File

@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./countInsight';
import { IInsight, InsightType } from './interfaces';
import { IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces';
import { $ } from 'vs/base/browser/dom';
import { Builder } from 'vs/base/browser/builder';
export class CountInsight implements IInsight {
public options;
public static readonly types = [InsightType.Count];
public readonly types = CountInsight.types;
private countImage: HTMLElement;
constructor(container: HTMLElement, options: any) {
this.countImage = $('div');
container.appendChild(this.countImage);
}
public layout() { }
set data(data: IInsightData) {
new Builder(this.countImage).empty();
for (let i = 0; i < data.columns.length; i++) {
let container = $('div.count-label-container');
let label = $('span.label-container');
label.innerText = data.columns[i];
let value = $('span.value-container');
value.innerText = data.rows[0][i];
container.appendChild(label);
container.appendChild(value);
this.countImage.appendChild(container);
}
}
dispose() {
}
}

View File

@@ -0,0 +1,321 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Chart as ChartJs } from 'chart.js';
import { mixin } from 'vs/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';
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
import { ChartType, DataDirection, LegendPosition } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import { IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces';
import { IInsightOptions, IInsight } from './interfaces';
const noneLineGraphs = [ChartType.Doughnut, ChartType.Pie];
const defaultOptions: IInsightOptions = {
type: ChartType.Bar,
dataDirection: DataDirection.Horizontal
};
export class Graph implements IInsight {
private _options: IInsightOptions;
private canvas: HTMLCanvasElement;
private chartjs: ChartJs;
private _data: IInsightData;
public static readonly types = [ChartType.Bar, ChartType.Doughnut, ChartType.HorizontalBar, ChartType.Line, ChartType.Pie, ChartType.Scatter, ChartType.TimeSeries];
public readonly types = Graph.types;
private _theme: ITheme;
constructor(
container: HTMLElement, options: IInsightOptions = defaultOptions,
@IThemeService themeService: IThemeService
) {
this._theme = themeService.getTheme();
themeService.onThemeChange(e => {
this._theme = e;
this.data = this._data;
});
this._options = mixin(options, defaultOptions, false);
let canvasContainer = document.createElement('div');
canvasContainer.style.width = '100%';
canvasContainer.style.height = '100%';
this.canvas = document.createElement('canvas');
canvasContainer.appendChild(this.canvas);
container.appendChild(canvasContainer);
}
public dispose() {
}
public layout() {
}
public set data(data: IInsightData) {
this._data = data;
let chartData: Array<ChartJs.ChartDataSets>;
let labels: Array<string>;
if (this.options.dataDirection === DataDirection.Horizontal) {
if (this.options.labelFirstColumn) {
labels = data.columns.slice(1);
} else {
labels = data.columns;
}
} else {
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)
};
});
}
} 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]
};
});
} 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)
};
});
}
}
chartData = chartData.map((c, i) => {
return mixin(c, getColors(this.options.type, i, c.data.length), false);
});
if (this.chartjs) {
this.chartjs.data.datasets = chartData;
this.chartjs.config.type = this.options.type;
this.chartjs.data.labels = labels;
this.chartjs.options = this.transformOptions(this.options);
this.chartjs.update(0);
} else {
this.chartjs = new ChartJs(this.canvas.getContext('2d'), {
data: {
labels: labels,
datasets: chartData
},
type: this.options.type,
options: {
maintainAspectRatio: false
}
});
}
}
private transformOptions(options: IInsightOptions): ChartJs.ChartOptions {
let retval: ChartJs.ChartOptions = {};
retval.maintainAspectRatio = false;
let foregroundColor = this._theme.getColor(colors.editorForeground);
let foreground = foregroundColor ? foregroundColor.toString() : null;
let gridLinesColor = this._theme.getColor(editorLineNumbers);
let gridLines = gridLinesColor ? gridLinesColor.toString() : null;
if (options) {
retval.scales = {};
// we only want to include axis if it is a axis based graph type
if (!noneLineGraphs.includes(options.type as ChartType)) {
retval.scales.xAxes = [{
scaleLabel: {
fontColor: foreground,
labelString: options.xAxisLabel,
display: options.xAxisLabel ? true : false
},
ticks: {
fontColor: foreground,
max: options.xAxisMax,
min: options.xAxisMin
},
gridLines: {
color: gridLines
}
}];
retval.scales.yAxes = [{
scaleLabel: {
fontColor: foreground,
labelString: options.yAxisLabel,
display: options.yAxisLabel ? true : false
},
ticks: {
fontColor: foreground,
max: options.yAxisMax,
min: options.yAxisMin
},
gridLines: {
color: gridLines
}
}];
}
retval.legend = {
position: options.legendPosition as ChartJs.PositionType,
display: options.legendPosition !== LegendPosition.None
};
}
return retval;
}
public set options(options: IInsightOptions) {
this._options = options;
this.data = this._data;
}
public get options(): IInsightOptions {
return this._options;
}
}
/**
* The Following code is pulled from ng2-charting in order to keep the same
* color functionality
*/
const defaultColors = [
[255, 99, 132],
[54, 162, 235],
[255, 206, 86],
[231, 233, 237],
[75, 192, 192],
[151, 187, 205],
[220, 220, 220],
[247, 70, 74],
[70, 191, 189],
[253, 180, 92],
[148, 159, 177],
[77, 83, 96]
];
function rgba(colour, alpha) {
return 'rgba(' + colour.concat(alpha).join(',') + ')';
}
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function getRandomColor() {
return [getRandomInt(0, 255), getRandomInt(0, 255), getRandomInt(0, 255)];
}
/**
* Generate colors for line|bar charts
* @param index
* @returns {number[]|Color}
*/
function generateColor(index) {
return defaultColors[index] || getRandomColor();
}
/**
* Generate colors for pie|doughnut charts
* @param count
* @returns {Colors}
*/
function generateColors(count) {
var colorsArr = new Array(count);
for (var i = 0; i < count; i++) {
colorsArr[i] = defaultColors[i] || getRandomColor();
}
return colorsArr;
}
function formatLineColor(colors) {
return {
backgroundColor: rgba(colors, 0.4),
borderColor: rgba(colors, 1),
pointBackgroundColor: rgba(colors, 1),
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: rgba(colors, 0.8)
};
}
function formatBarColor(colors) {
return {
backgroundColor: rgba(colors, 0.6),
borderColor: rgba(colors, 1),
hoverBackgroundColor: rgba(colors, 0.8),
hoverBorderColor: rgba(colors, 1)
};
}
function formatPieColors(colors) {
return {
backgroundColor: colors.map(function (color) { return rgba(color, 0.6); }),
borderColor: colors.map(function () { return '#fff'; }),
pointBackgroundColor: colors.map(function (color) { return rgba(color, 1); }),
pointBorderColor: colors.map(function () { return '#fff'; }),
pointHoverBackgroundColor: colors.map(function (color) { return rgba(color, 1); }),
pointHoverBorderColor: colors.map(function (color) { return rgba(color, 1); })
};
}
function formatPolarAreaColors(colors) {
return {
backgroundColor: colors.map(function (color) { return rgba(color, 0.6); }),
borderColor: colors.map(function (color) { return rgba(color, 1); }),
hoverBackgroundColor: colors.map(function (color) { return rgba(color, 0.8); }),
hoverBorderColor: colors.map(function (color) { return rgba(color, 1); })
};
}
/**
* Generate colors by chart type
* @param chartType
* @param index
* @param count
* @returns {Color}
*/
function getColors(chartType, index, count) {
if (chartType === 'pie' || chartType === 'doughnut') {
return formatPieColors(generateColors(count));
}
if (chartType === 'polarArea') {
return formatPolarAreaColors(generateColors(count));
}
if (chartType === 'line' || chartType === 'radar') {
return formatLineColor(generateColor(index));
}
if (chartType === 'bar' || chartType === 'horizontalBar') {
return formatBarColor(generateColor(index));
}
return generateColor(index);
}

View File

@@ -0,0 +1,71 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInsight, IInsightOptions, InsightType } from './interfaces';
import { IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces';
import { $ } from 'vs/base/browser/dom';
import { mixin } from 'vs/base/common/objects';
export interface IConfig extends IInsightOptions {
encoding?: string;
imageFormat?: string;
}
const defaultConfig: IConfig = {
type: InsightType.Image,
encoding: 'hex',
imageFormat: 'jpeg'
};
export class ImageInsight implements IInsight {
public static readonly types = [InsightType.Image];
public readonly types = ImageInsight.types;
private _options: IConfig;
private imageEle: HTMLImageElement;
constructor(container: HTMLElement, options: IConfig) {
this.imageEle = $('img');
container.appendChild(this.imageEle);
}
public layout() {
}
public dispose() {
}
set options(config: IConfig) {
this._options = mixin(config, defaultConfig, false);
}
get options(): IConfig {
return this._options;
}
set data(data: IInsightData) {
if (data.rows && data.rows.length > 0 && data.rows[0].length > 0) {
let img = data.rows[0][0];
if (this._options.encoding === 'hex') {
img = ImageInsight._hexToBase64(img);
}
this.imageEle.src = `data:image/${this._options.imageFormat};base64,${img}`;
}
}
private static _hexToBase64(hexVal: string) {
if (hexVal.startsWith('0x')) {
hexVal = hexVal.slice(2);
}
// should be able to be replaced with new Buffer(hexVal, 'hex').toString('base64')
return btoa(String.fromCharCode.apply(null, hexVal.replace(/\r|\n/g, '').replace(/([\da-fA-F]{2}) ?/g, '0x$1 ').replace(/ +$/, '').split(' ')));
}
}

View File

@@ -0,0 +1,98 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Graph } from './graphInsight';
import { ChartType, DataDirection } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import { IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces';
import { Builder } from 'vs/base/browser/builder';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ImageInsight } from './imageInsight';
import { TableInsight } from './tableInsight';
import { IInsightOptions, IInsight, InsightType, IInsightCtor } from './interfaces';
import { CountInsight } from './countInsight';
import { Dimension } from 'vs/base/browser/dom';
const defaultOptions: IInsightOptions = {
type: ChartType.Bar,
dataDirection: DataDirection.Horizontal
};
export class Insight {
private insight: IInsight;
private _options: IInsightOptions;
private _data: IInsightData;
private dim: Dimension
constructor(
private container: HTMLElement, options: IInsightOptions = defaultOptions,
@IInstantiationService private _instantiationService: IInstantiationService
) {
this.options = options;
this.buildInsight();
}
public layout(dim: Dimension) {
this.dim = dim;
this.insight.layout(dim);
}
public set options(val: IInsightOptions) {
this._options = val;
if (this.insight) {
// check to see if we need to change the insight type
if (!this.insight.types.includes(val.type)) {
this.buildInsight();
} else {
this.insight.options = val;
}
}
}
public get options(): IInsightOptions {
return this._options;
}
public set data(val: IInsightData) {
this._data = val;
if (this.insight) {
this.insight.data = val;
}
}
private buildInsight() {
if (this.insight) {
this.insight.dispose();
}
new Builder(this.container).empty();
let ctor = this.findctor(this.options.type);
if (ctor) {
this.insight = this._instantiationService.createInstance(ctor, this.container, this.options);
this.insight.layout(this.dim);
if (this._data) {
this.insight.data = this._data;
}
}
}
private findctor(type: ChartType | InsightType): IInsightCtor {
if (Graph.types.includes(type as ChartType)) {
return Graph;
} else if (ImageInsight.types.includes(type as InsightType)) {
return ImageInsight;
} else if (TableInsight.types.includes(type as InsightType)) {
return TableInsight;
} else if (CountInsight.types.includes(type as InsightType)) {
return CountInsight;
}
return undefined;
}
}

View File

@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces';
import { ChartType, LegendPosition, DataDirection } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import { Dimension } from 'vs/base/browser/dom';
import { DataType } from 'sql/parts/dashboard/widgets/insights/views/charts/types/lineChart.component';
export interface IInsightOptions {
type: InsightType | ChartType;
dataDirection?: DataDirection;
dataType?: DataType;
labelFirstColumn?: boolean;
columnsAsLabels?: boolean;
legendPosition?: LegendPosition;
yAxisLabel?: string;
yAxisMin?: number;
yAxisMax?: number;
xAxisLabel?: string;
xAxisMin?: number;
xAxisMax?: number;
encoding?: string;
imageFormat?: string;
}
export interface IInsight {
options: IInsightOptions;
data: IInsightData;
readonly types: Array<InsightType | ChartType>;
layout(dim: Dimension);
dispose();
}
export interface IInsightCtor {
new (container: HTMLElement, options: IInsightOptions, ...services: { _serviceBrand: any; }[]): IInsight;
readonly types: Array<InsightType | ChartType>;
}
export enum InsightType {
Image = 'image',
Table = 'table',
Count = 'count'
}

View File

@@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IInsight, InsightType } from './interfaces';
import { IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { Table } from 'sql/base/browser/ui/table/table';
import { attachTableStyler } from 'sql/common/theme/styler';
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
import { $, Dimension } from 'vs/base/browser/dom';
import { Disposable } from 'vs/base/common/lifecycle';
import { IThemeService } from 'vs/platform/theme/common/themeService';
export class TableInsight extends Disposable implements IInsight {
public static readonly types = [InsightType.Table];
public readonly types = TableInsight.types;
private table: Table<any>;
private dataView: TableDataView<any>;
private columns: Slick.Column<any>[];
constructor(container: HTMLElement, options: any,
@IThemeService themeService: IThemeService
) {
super();
let tableContainer = $('div');
tableContainer.style.width = '100%';
tableContainer.style.height = '100%';
container.appendChild(tableContainer);
this.dataView = new TableDataView();
this.table = new Table(tableContainer, { dataProvider: this.dataView }, { showRowNumber: true });
this.table.setSelectionModel(new CellSelectionModel());
this._register(attachTableStyler(this.table, themeService));
}
set data(data: IInsightData) {
this.dataView.clear();
this.dataView.push(transformData(data.rows, data.columns));
this.columns = transformColumns(data.columns);
this.table.columns = this.columns;
}
layout(dim: Dimension) {
this.table.layout(dim);
}
public options;
}
function transformData(rows: string[][], columns: string[]): { [key: string]: string }[] {
return rows.map(row => {
let object: { [key: string]: string } = {};
row.forEach((val, index) => {
object[columns[index]] = val;
});
return object;
});
}
function transformColumns(columns: string[]): Slick.Column<any>[] {
return columns.map(col => {
return <Slick.Column<any>>{
name: col,
id: col,
field: col
};
});
}

View File

@@ -12,7 +12,7 @@ import { ScrollableSplitView } from 'sql/base/browser/ui/scrollableSplitview/scr
import { MouseWheelSupport } from 'sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin';
import { AutoColumnSize } from 'sql/base/browser/ui/table/plugins/autoSizeColumns.plugin';
import { SaveFormat } from 'sql/parts/grid/common/interfaces';
import { IGridActionContext, SaveResultAction, CopyResultAction, SelectAllGridAction, MaximizeTableAction, MinimizeTableAction } from 'sql/parts/query/editor/actions';
import { IGridActionContext, SaveResultAction, CopyResultAction, SelectAllGridAction, MaximizeTableAction, MinimizeTableAction, ChartDataAction } from 'sql/parts/query/editor/actions';
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
import { RowNumberColumn } from 'sql/base/browser/ui/table/plugins/rowNumberColumn.plugin';
@@ -33,6 +33,7 @@ import { generateUuid } from 'vs/base/common/uuid';
import { TPromise } from 'vs/base/common/winjs.base';
import { Separator, ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { Dimension, getContentWidth } from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
const rowHeight = 29;
const columnHeight = 26;
@@ -93,7 +94,8 @@ export class GridPanel extends ViewletPanel {
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@IConfigurationService configurationService: IConfigurationService,
@IThemeService private themeService: IThemeService
@IThemeService private themeService: IThemeService,
@IInstantiationService private instantiationService: IInstantiationService
) {
super(title, options, keybindingService, contextMenuService, configurationService);
this.splitView = new ScrollableSplitView(this.container, { enableResizing: false });
@@ -143,7 +145,7 @@ export class GridPanel extends ViewletPanel {
for (let set of resultsToAdd) {
let tableState = new GridTableState();
let table = new GridTable(this.runner, tableState, set, this.contextMenuService);
let table = new GridTable(this.runner, tableState, set, this.contextMenuService, this.instantiationService);
tableState.onMaximizedChange(e => {
if (e) {
this.maximizeTable(table.id);
@@ -222,7 +224,8 @@ class GridTable<T> extends Disposable implements IView {
private runner: QueryRunner,
public state: GridTableState,
private resultSet: sqlops.ResultSetSummary,
private contextMenuService: IContextMenuService
private contextMenuService: IContextMenuService,
private instantiationService: IInstantiationService
) {
super();
this.container.style.width = '100%';
@@ -283,7 +286,8 @@ class GridTable<T> extends Disposable implements IView {
actions.push(
new SaveResultAction(SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV),
new SaveResultAction(SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL),
new SaveResultAction(SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON)
new SaveResultAction(SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON),
this.instantiationService.createInstance(ChartDataAction)
);
let actionBarContainer = document.createElement('div');

View File

@@ -151,6 +151,10 @@ export class QueryResultsEditor extends BaseEditor {
return TPromise.wrap<void>(null);
}
public chart(dataId: { batchId: number, resultId: number }) {
this.resultsView.chartData(dataId);
}
public dispose(): void {
super.dispose();
}

View File

@@ -16,8 +16,7 @@ import * as UUID from 'vs/base/common/uuid';
import { PanelViewlet } from 'vs/workbench/browser/parts/views/panelViewlet';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import * as DOM from 'vs/base/browser/dom';
import { Emitter } from 'vs/base/common/event';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ChartTab } from './charting/chartTab';
class ResultsView implements IPanelView {
private panelViewlet: PanelViewlet;
@@ -25,14 +24,6 @@ class ResultsView implements IPanelView {
private messagePanel: MessagePanel;
private container = document.createElement('div');
private _onRemove = new Emitter<void>();
public readonly onRemove = this._onRemove.event;
private _onLayout = new Emitter<void>();
public readonly onLayout = this._onLayout.event;
private queryRunnerDisposable: IDisposable[] = [];
constructor(instantiationService: IInstantiationService) {
this.panelViewlet = instantiationService.createInstance(PanelViewlet, 'resultsView', { showHeaderInTitleWhenSingleView: false });
this.gridPanel = instantiationService.createInstance(GridPanel, nls.localize('gridPanel', 'Results'), {});
@@ -61,6 +52,10 @@ class ResultsView implements IPanelView {
this.gridPanel.queryRunner = runner;
this.messagePanel.queryRunner = runner;
}
public hideResultHeader() {
this.gridPanel.headerVisible = false;
}
}
class ResultsTab implements IPanelTab {
@@ -68,17 +63,8 @@ class ResultsTab implements IPanelTab {
public readonly identifier = UUID.generateUuid();
public readonly view: ResultsView;
private _isAttached = false;
constructor(instantiationService: IInstantiationService) {
this.view = new ResultsView(instantiationService);
this.view.onLayout(() => this._isAttached = true, this);
this.view.onRemove(() => this._isAttached = false, this);
}
public isAttached(): boolean {
return this._isAttached;
}
public set queryRunner(runner: QueryRunner) {
@@ -90,6 +76,7 @@ export class QueryResultsView {
private _panelView: TabbedPanel;
private _input: QueryResultsInput;
private resultsTab: ResultsTab;
private chartTab: ChartTab;
constructor(
container: HTMLElement,
@@ -97,19 +84,20 @@ export class QueryResultsView {
@IQueryModelService private queryModelService: IQueryModelService
) {
this.resultsTab = new ResultsTab(instantiationService);
this.chartTab = new ChartTab(instantiationService);
this._panelView = new TabbedPanel(container, { showHeaderWhenSingleView: false });
}
public style() {
}
public set input(input: QueryResultsInput) {
this._input = input;
this.resultsTab.queryRunner = this.queryModelService._getQueryInfo(input.uri).queryRunner;
// if (!this.resultsTab.isAttached) {
let queryRunner = this.queryModelService._getQueryInfo(input.uri).queryRunner;
this.resultsTab.queryRunner = queryRunner;
this.chartTab.queryRunner = queryRunner;
this._panelView.pushTab(this.resultsTab);
// }
}
public get input(): QueryResultsInput {
@@ -119,4 +107,14 @@ export class QueryResultsView {
public layout(dimension: DOM.Dimension) {
this._panelView.layout(dimension);
}
public chartData(dataId: { resultId: number, batchId: number }): void {
if (!this._panelView.contains(this.chartTab)) {
this._panelView.pushTab(this.chartTab);
this.resultsTab.view.hideResultHeader();
}
this._panelView.showTab(this.chartTab.identifier);
this.chartTab.chart(dataId);
}
}