mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-27 01:25:36 -05:00
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:
@@ -142,3 +142,8 @@ panel {
|
||||
.visibility.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.tabBody {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) { }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
204
src/sql/parts/query/editor/charting/chartOptions.ts
Normal file
204
src/sql/parts/query/editor/charting/chartOptions.ts
Normal 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'
|
||||
}
|
||||
]
|
||||
};
|
||||
32
src/sql/parts/query/editor/charting/chartTab.ts
Normal file
32
src/sql/parts/query/editor/charting/chartTab.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
19
src/sql/parts/query/editor/charting/chartView.css
Normal file
19
src/sql/parts/query/editor/charting/chartView.css
Normal 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;
|
||||
}
|
||||
240
src/sql/parts/query/editor/charting/chartView.ts
Normal file
240
src/sql/parts/query/editor/charting/chartView.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
45
src/sql/parts/query/editor/charting/insights/countInsight.ts
Normal file
45
src/sql/parts/query/editor/charting/insights/countInsight.ts
Normal 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() {
|
||||
|
||||
}
|
||||
}
|
||||
321
src/sql/parts/query/editor/charting/insights/graphInsight.ts
Normal file
321
src/sql/parts/query/editor/charting/insights/graphInsight.ts
Normal 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);
|
||||
}
|
||||
71
src/sql/parts/query/editor/charting/insights/imageInsight.ts
Normal file
71
src/sql/parts/query/editor/charting/insights/imageInsight.ts
Normal 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(' ')));
|
||||
}
|
||||
}
|
||||
98
src/sql/parts/query/editor/charting/insights/insight.ts
Normal file
98
src/sql/parts/query/editor/charting/insights/insight.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
47
src/sql/parts/query/editor/charting/insights/interfaces.ts
Normal file
47
src/sql/parts/query/editor/charting/insights/interfaces.ts
Normal 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'
|
||||
}
|
||||
74
src/sql/parts/query/editor/charting/insights/tableInsight.ts
Normal file
74
src/sql/parts/query/editor/charting/insights/tableInsight.ts
Normal 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
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user