From cbbde3387b8911d95a590949cc0208df2c3babaf Mon Sep 17 00:00:00 2001 From: Benjin Dubishar Date: Wed, 13 Sep 2023 20:11:09 -0700 Subject: [PATCH] Adding Chart component (#24357) * added doughnut chart component * Changing chart to doughnutChart * reverting to genreic chart component * adding more chart supoort * fix minor errors * resolve some PR comments * native chartjs, keyboard navigation and chart options * fix build errors * fix chart.js/auto error * resolve PR comments * modify chartdataset API * Refactoring (#24327) * working - displaying chart data with convert * working - introduced typed properties * working, added BarChartConfiguration to type param * removed ChartProperties type param * Adding doughnut support * Correcting number vs. point issue * including the right changes this time * commenting out no-longer-used labels prop * remove hardcoded canvasID, enabled Scatterplot config * Moved graph testing to sample extension * Reorganizing types; adding test back to assessment dialog * Adding example for bubble chart * Polar area working * cleanup * adding draw when options isn't set * Moving chart example configs to other file * some cleanup * added some docstrings * add multiple datasets to test scatter plot * update scatter plot example in sample * Adding height/width support * swapping to `as` cast * title working * Settling chart title and legend display * Adding comments * updating data working * Updating samples * Typo in comment * Reverting changes made for development * Elaborating on color in docstrings * Separating Data and Options in component payloads * Removing chartId as an exposed property * Changing chartType property to TChartType * Fleshing out types file comments * fixing scoping of chart component properties; renaming chart canvas ID prop * correct internal chart options typing * removing commented-out code * removing unused ChartClickEvent type until data selection eventing is implemented * renaming function * deleted commented-out code * Adding options setters that went missing after splitting Config to Data + Options * adding type predicates for data conversion * Adding back type setting (dropped when chart type conversion moved) * Narrowing type for 'type' * Fixing typos in docstring --------- Co-authored-by: Deepak Saini Co-authored-by: Charles Gagnon Co-authored-by: Aasim Khan Co-authored-by: Deepak Saini --- extensions/dacpac/src/test/testContext.ts | 1 + .../datavirtualization/src/test/stubs.ts | 3 + .../machine-learning/src/test/views/utils.ts | 1 + .../managePackagesDialog.test.ts | 1 + extensions/query-store/src/test/testUtils.ts | 1 + .../schema-compare/src/test/testContext.ts | 1 + samples/sqlservices/README.md | 3 +- samples/sqlservices/src/chartExamples.ts | 312 +++++++++++++ .../src/controllers/mainController.ts | 143 +++++- src/sql/azdata.proposed.d.ts | 424 ++++++++++++++++++ .../browser/ui/chart/chart.component.html | 5 + .../base/browser/ui/chart/chart.component.ts | 366 +++++++++++++++ src/sql/base/browser/ui/chart/chart.module.ts | 19 + .../platform/dashboard/browser/interfaces.ts | 3 +- .../workbench/api/common/extHostModelView.ts | 45 ++ .../workbench/api/common/sqlExtHostTypes.ts | 3 +- .../modelComponents/chart.component.html | 3 + .../modelComponents/chart.component.ts | 81 ++++ .../dashboard/browser/dashboard.module.ts | 4 +- .../browser/components.contribution.ts | 4 + .../services/dialog/browser/dialog.module.ts | 4 +- 21 files changed, 1419 insertions(+), 8 deletions(-) create mode 100644 samples/sqlservices/src/chartExamples.ts create mode 100644 src/sql/base/browser/ui/chart/chart.component.html create mode 100644 src/sql/base/browser/ui/chart/chart.component.ts create mode 100644 src/sql/base/browser/ui/chart/chart.module.ts create mode 100644 src/sql/workbench/browser/modelComponents/chart.component.html create mode 100644 src/sql/workbench/browser/modelComponents/chart.component.ts diff --git a/extensions/dacpac/src/test/testContext.ts b/extensions/dacpac/src/test/testContext.ts index 8a23ecada8ec..b03d4299f530 100644 --- a/extensions/dacpac/src/test/testContext.ts +++ b/extensions/dacpac/src/test/testContext.ts @@ -288,6 +288,7 @@ export function createViewContext(): ViewTestContext { modelBuilder: { listView: undefined!, radioCardGroup: undefined!, + chart: undefined!, navContainer: undefined!, divContainer: () => divBuilder, flexContainer: () => flexBuilder, diff --git a/extensions/datavirtualization/src/test/stubs.ts b/extensions/datavirtualization/src/test/stubs.ts index 7c21926151f7..4e1e9fd4dbd1 100644 --- a/extensions/datavirtualization/src/test/stubs.ts +++ b/extensions/datavirtualization/src/test/stubs.ts @@ -597,6 +597,9 @@ export class MockModelBuilder implements azdata.ModelBuilder { listView(): azdata.ComponentBuilder { throw new Error('Method not implemented.'); } + chart, TOptions extends azdata.ChartOptions>(): azdata.ComponentBuilder, azdata.ChartComponentProperties> { + throw new Error('Method not implemented.'); + } slider(): azdata.ComponentBuilder { throw new Error('Method not implemented.'); } diff --git a/extensions/machine-learning/src/test/views/utils.ts b/extensions/machine-learning/src/test/views/utils.ts index 662172593a76..b54174f63501 100644 --- a/extensions/machine-learning/src/test/views/utils.ts +++ b/extensions/machine-learning/src/test/views/utils.ts @@ -234,6 +234,7 @@ export function createViewContext(): ViewTestContext { modelBuilder: { listView: undefined!, radioCardGroup: undefined!, + chart: undefined!, navContainer: undefined!, divContainer: () => divBuilder, flexContainer: () => flexBuilder, diff --git a/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts b/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts index cdb462b4ed49..0c62feecaf33 100644 --- a/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts +++ b/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts @@ -274,6 +274,7 @@ describe('Manage Package Dialog', () => { modelBuilder: { listView: undefined!, radioCardGroup: undefined!, + chart: undefined!, navContainer: undefined!, divContainer: undefined!, flexContainer: () => flexBuilder, diff --git a/extensions/query-store/src/test/testUtils.ts b/extensions/query-store/src/test/testUtils.ts index 8f6a0c67b1ad..e99b37a649ad 100644 --- a/extensions/query-store/src/test/testUtils.ts +++ b/extensions/query-store/src/test/testUtils.ts @@ -72,6 +72,7 @@ export function createViewContext(): TestContext { modelBuilder: { listView: undefined!, radioCardGroup: undefined!, + chart: undefined!, navContainer: undefined!, divContainer: undefined!, flexContainer: () => flexBuilder, diff --git a/extensions/schema-compare/src/test/testContext.ts b/extensions/schema-compare/src/test/testContext.ts index e30e4fb9a9b9..f3e3d7db197e 100644 --- a/extensions/schema-compare/src/test/testContext.ts +++ b/extensions/schema-compare/src/test/testContext.ts @@ -330,6 +330,7 @@ export function createViewContext(): ViewTestContext { modelBuilder: { listView: undefined!, radioCardGroup: undefined!, + chart: undefined!, navContainer: undefined!, divContainer: () => divBuilder, flexContainer: () => flexBuilder, diff --git a/samples/sqlservices/README.md b/samples/sqlservices/README.md index e7ba09c15470..46e03f56f85c 100644 --- a/samples/sqlservices/README.md +++ b/samples/sqlservices/README.md @@ -10,8 +10,7 @@ This is a sample extension that will show some basic model-backed UI scenarios a - `yarn install` - to install dependencies - `yarn build` - to build the code - Launch VSCode and open the azuredatastudio's code folder, run the 'Launch azuredatastudio' debug option (to work around the issue. The next step won't work without doing this first) -- Launch VSCode and open this folder, run the 'Debug in enlistment'. To debug, [install the `sqlops-debug` extension](https://github.com/Microsoft/azuredatastudio/wiki/Debugging-an-Extension-with-VS-Code) in VS Code. -- Launch VSCode and open this folder, run the 'Debug in enlistment' +- Launch VSCode and open this folder, run the 'Debug in enlistment' target. To debug, [install the `sqlops-debug` extension](https://github.com/Microsoft/azuredatastudio/wiki/Debugging-an-Extension-with-VS-Code) in VS Code. - Once ADS launches, you should be able to run the sqlservices commands, for example: `sqlservices.openDialog`. ## Consuming `azdata` typing changes during development diff --git a/samples/sqlservices/src/chartExamples.ts b/samples/sqlservices/src/chartExamples.ts new file mode 100644 index 000000000000..5dceb231edef --- /dev/null +++ b/samples/sqlservices/src/chartExamples.ts @@ -0,0 +1,312 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; + +export const barData: azdata.BarChartData = { + datasets: [ + { + data: [3, 4, 5, 4], + backgroundColor: 'rgb(0, 0, 0, 0.8)', + borderColor: 'black', + dataLabel: 'Black Stripes' + }, + { + data: [4, 4.5, 4, 3.5], + backgroundColor: 'rgb(255, 255, 0, 0.8)', + borderColor: 'yellow', + dataLabel: 'Yellow Stripes' + }, + { + data: [5, 3.5, 3, 4], + backgroundColor: 'rgb(255, 0, 0, 0.8)', + borderColor: 'red', + dataLabel: 'Red Stripes' + } + ], + labels: ['Een', 'Twee', 'Drie', 'Vier'] +}; + +export const barOptions: azdata.BarChartOptions = { + chartTitle: 'Test Bar Chart - Belgian Flag', + scales: { + x: { + max: 8 + } + } +}; + +export const horizontalBarData: azdata.HorizontalBarChartData = { + datasets: [ + { + data: [3, 8], + backgroundColor: '#FF8800BB', + borderColor: 'orange', + dataLabel: 'Orange Stripes' + }, + { + data: [3.5, 7], + backgroundColor: '#FFFFFFBB', + borderColor: 'white', + dataLabel: 'White Stripes' + }, + { + data: [4, 9], + backgroundColor: '#008800BB', + borderColor: '#008800', + dataLabel: 'Green Stripes' + } + ], + labels: ['Ek', 'Do'] +}; + +export const horizontalBarOptions: azdata.HorizontalBarChartOptions = { + chartTitle: 'Test Horizontal Bar Chart - Indian Flag', + scales: { + x: { + max: 8 + } + } +}; + +export const lineData: azdata.LineChartData = { + datasets: [ + { + data: [2, 3, 4], + backgroundColor: '#FFFF88', + borderColor: '#FFFF00', + dataLabel: 'By One' + }, + { + data: [3.5, 4, 4.5], + backgroundColor: '#88FFFF', + borderColor: '#00FFFF', + dataLabel: 'By Half' + }, + { + data: [1, 3, 5], + backgroundColor: '#FF88FF', + borderColor: '#FF00FF', + dataLabel: 'By Two' + } + ], + labels: ['uno', 'dos', 'tres', 'quatro'] +}; + +export const lineOptions: azdata.LineChartOptions = { + chartTitle: 'Test Line Chart', + scales: { + x: { + max: 8 + } + } +}; + +export const pieData: azdata.PieChartData = { + dataset: [ + { + value: 3, + backgroundColor: 'rgb(255, 255, 0, 0.5)', + borderColor: 'yellow', + dataLabel: 'Pacman' + }, + { + value: 1, + backgroundColor: 'rgb(50, 50, 50, 0.5)', + borderColor: 'black', + dataLabel: 'Not Pacman' + } + ] +}; + +export const pieOptions: azdata.PieChartOptions = { + chartTitle: 'Test Pie Chart - Pacman', + rotation: 135 +}; + +export const doughnutData: azdata.DoughnutChartData = { + dataset: [ + { + value: 50, + backgroundColor: 'rgb(50, 50, 50, 0.5)', + borderColor: 'black', + dataLabel: 'Eaten' + }, + { + value: 100, + backgroundColor: 'rgb(180, 130, 85, 0.5)', + borderColor: 'brown', + dataLabel: 'No Icing' + }, + { + value: 300, + backgroundColor: 'rgb(255, 150, 200, 0.5)', + borderColor: 'pink', + dataLabel: 'Icing' + } + ] +}; + +export const doughnutOptions: azdata.DoughnutChartOptions = { + chartTitle: 'Test Doughnut Chart - Strawberry Doughnut' +}; + +export const scatterData: azdata.ScatterplotData = { + datasets: [ + { + data: [ + { x: -10, y: 0 }, + { x: 0, y: 10 }, + { x: 10, y: 5 }, + { x: 0.5, y: 5.5 } + ], + backgroundColor: 'rgb(255, 99, 132)', + borderColor: 'rgb(0, 255, 132)', + dataLabel: 'Rojo' + }, + { + data: + [ + { x: -5, y: 2 }, + { x: 4, y: 8 }, + { x: -1, y: 6 } + ], + backgroundColor: 'rgb(0, 102, 204)', + borderColor: 'rgb(0, 102, 204)', + dataLabel: 'Azul' + } + ] +}; + +export const scatterOptions: azdata.ScatterplotOptions = { + chartTitle: 'Test Scatter Chart', + scales: { + x: { + position: 'bottom' + } + } +}; + +export const bubbleData: azdata.BubbleChartData = { + datasets: [ + { + data: + [ + { x: 0, y: -5, r: 2 }, + { x: -2, y: -4.6, r: 4 }, + { x: -3.5, y: -3.5, r: 6 }, + { x: -4.6, y: -2, r: 8 }, + { x: -5, y: 0, r: 10 }, + { x: -4.6, y: 2, r: 12 }, + { x: -3.5, y: 3.5, r: 14 }, + { x: -2, y: 4.6, r: 16 }, + { x: 0, y: 5, r: 18 } + ], + backgroundColor: '#FFFFFF88', + borderColor: 'white', + dataLabel: 'Yin' + }, + { + data: + [ + { x: 0, y: 5, r: 2 }, + { x: 2, y: 4.6, r: 4 }, + { x: 3.5, y: 3.5, r: 6 }, + { x: 4.6, y: 2, r: 8 }, + { x: 5, y: 0, r: 10 }, + { x: 4.6, y: -2, r: 12 }, + { x: 3.5, y: -3.5, r: 14 }, + { x: 2, y: -4.6, r: 16 }, + { x: 0, y: -5, r: 18 } + ], + backgroundColor: '#00000088', + borderColor: 'black', + dataLabel: 'Yang' + } + ] +}; + +export const bubbleOptions: azdata.BubbleChartOptions = { + chartTitle: 'Test Bubble Chart - Yin and Yang', + scales: { + x: { + position: 'bottom' + } + } +}; + +export const polarData: azdata.PolarAreaChartData = { + dataset: + [ + { + value: 1, + dataLabel: 'Rouge', + backgroundColor: '#FF0000', + borderColor: '#880000' + }, + { + value: 2, + dataLabel: 'Orange', + backgroundColor: '#FF8800', + borderColor: '#884400' + }, + { + value: 3, + dataLabel: 'Jaune', + backgroundColor: '#FFFF00', + borderColor: '#888800' + }, + { + value: 4, + dataLabel: 'Vert', + backgroundColor: '#00FF00', + borderColor: '#008800' + }, + { + value: 5, + dataLabel: 'Bleu', + backgroundColor: '#0000FF', + borderColor: '#000088' + }, + { + value: 6, + dataLabel: 'Violet', + backgroundColor: '#8800FF', + borderColor: '#440088' + } + ] +}; + +export const polarOptions: azdata.PolarAreaChartOptions = { + chartTitle: 'Test Polar Chart - Rainbow' +}; + +export const radarData: azdata.RadarChartData = { + datasets: [ + { + data: [2, 2, 2, 2, 4, 7, 10, 11, 12, 2], + dataLabel: 'Left Wing', + backgroundColor: '#FF000033', + borderColor: '#FF0000' + }, + { + data: [2, 2, 12, 11, 10, 7, 4, 2, 2, 2], + dataLabel: 'Right Wing', + backgroundColor: '#FF880033', + borderColor: '#FF8800' + }, + { + data: [8, 6, 2, 1, 1, 1, 1, 1, 2, 6], + dataLabel: 'Head', + backgroundColor: '#FFFF0033', + borderColor: '#FFFF00' + } + ], + labels: ['She\'ll', 'Be', 'Coming', 'Around', 'The', 'Firebird', 'When', 'She', 'Comes', 'Encore'] +}; + +export const radarOptions: azdata.RadarChartOptions = { + chartTitle: 'Test Radar Chart - Firebird' +}; diff --git a/samples/sqlservices/src/controllers/mainController.ts b/samples/sqlservices/src/controllers/mainController.ts index 67a929cf69f8..e96a6ddb6bfc 100644 --- a/samples/sqlservices/src/controllers/mainController.ts +++ b/samples/sqlservices/src/controllers/mainController.ts @@ -16,6 +16,7 @@ import * as dashboard from './modelViewDashboard'; import { ConnectionProvider } from '../featureProviders/connectionProvider'; import { IconProvider } from '../featureProviders/iconProvider'; import { ObjectExplorerProvider } from '../featureProviders/objectExplorerProvider'; +import * as chartExamples from '../chartExamples'; /** * The main controller class that initializes the extension @@ -116,6 +117,12 @@ export default class MainController implements vscode.Disposable { }); dialog.content.push(treeTab); + const graphTab = azdata.window.createTab('Graphs'); + graphTab.registerContent(async (view) => { + await this.getGraphTabContent(view); + }); + dialog.content.push(graphTab); + // Open the dialog azdata.window.openDialog(dialog); @@ -701,9 +708,141 @@ export default class MainController implements vscode.Disposable { await view.initializeModel(formWrapper); } - //#endregion + private async getGraphTabContent(view: azdata.ModelView): Promise { + const barChart = view.modelBuilder.chart<'bar', azdata.BarChartData, azdata.BarChartOptions>() + .withProps({ + chartType: 'bar', + data: chartExamples.barData, + options: chartExamples.barOptions, + width: '500px', + height: '300px' + }).component(); + + const horizontalBarChart = view.modelBuilder.chart<'horizontalBar', azdata.HorizontalBarChartData, azdata.HorizontalBarChartOptions>() + .withProps({ + chartType: 'horizontalBar', + data: chartExamples.horizontalBarData, + options: chartExamples.horizontalBarOptions, + width: '500px', + height: '300px' + }).component(); + + const lineChart = view.modelBuilder.chart<'line', azdata.LineChartData, azdata.LineChartOptions>() + .withProps({ + chartType: 'line', + data: chartExamples.lineData, + options: chartExamples.lineOptions, + width: '500px', + height: '300px' + }).component(); + + const pieChart = view.modelBuilder.chart<'pie', azdata.PieChartData, azdata.PieChartOptions>() + .withProps({ + chartType: 'pie', + data: chartExamples.pieData, + options: chartExamples.pieOptions, + width: '300px', + height: '300px' + }).component(); + + const doughnutChart = view.modelBuilder.chart<'doughnut', azdata.DoughnutChartData, azdata.DoughnutChartOptions>() + .withProps({ + chartType: 'doughnut', + data: chartExamples.doughnutData, + options: chartExamples.doughnutOptions, + width: '400px', + height: '400px' + }).component(); + + const scatterplot = view.modelBuilder.chart<'scatter', azdata.ScatterplotData, azdata.ScatterplotOptions>() + .withProps({ + chartType: 'scatter', + data: chartExamples.scatterData, + options: chartExamples.scatterOptions, + width: '400px', + height: '400px' + }).component(); + + const bubbleChart = view.modelBuilder.chart<'bubble', azdata.BubbleChartData, azdata.BubbleChartOptions>() + .withProps({ + chartType: 'bubble', + data: chartExamples.bubbleData, + options: chartExamples.bubbleOptions, + width: '500px', + height: '500px' + }).component(); - //#region Registrations + const polarChart = view.modelBuilder.chart<'polarArea', azdata.PolarAreaChartData, azdata.PolarAreaChartOptions>() + .withProps({ + chartType: 'polarArea', + data: chartExamples.polarData, + options: chartExamples.polarOptions, + width: '500px', + height: '500px' + }).component(); + + const radarChart = view.modelBuilder.chart<'radar', azdata.RadarChartData, azdata.RadarChartOptions>() + .withProps({ + chartType: 'radar', + data: chartExamples.radarData, + options: chartExamples.radarOptions, + width: '500px', + height: '500px' + }).component(); + + const button = view.modelBuilder.button() + .withProps({ + label: 'Click to change bar chart data' + }).component(); + + button.onDidClick(async () => { + // To update data, a new data object must be created and passed. + // If the existing one is updated, it's detected as the same object, and "saves" the effort of send propertyChanged events. + + const newDataSets: azdata.BarChartDataSet[] = []; + + for (let i = 0; i < chartExamples.barData.datasets.length; i++) { + const newSet: azdata.BarChartDataSet = { + ...chartExamples.barData.datasets[i], // spread to preserve existing colors and label + data: [] + }; + + for (let j = 0; j < chartExamples.barData.datasets[i].data.length; j++) { + newSet.data.push(Math.random() * 8); + } + + newDataSets.push(newSet); + } + + const newData: azdata.BarChartData = { + labels: chartExamples.barData.labels, + datasets: newDataSets + }; + + await barChart.updateProperty('data', newData); + }); + + const flexContainer = view.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'column' }) + .withProps({ CSSStyles: { 'padding': '20px 15px' } }) + .component(); + + flexContainer.addItem(button, { flex: '0 0 auto' }); + flexContainer.addItem(barChart, { flex: '0 0 auto' }); + flexContainer.addItem(horizontalBarChart, { flex: '0 0 auto' }); + flexContainer.addItem(lineChart, { flex: '0 0 auto' }); + flexContainer.addItem(pieChart, { flex: '0 0 auto' }); + flexContainer.addItem(doughnutChart, { flex: '0 0 auto' }); + flexContainer.addItem(scatterplot, { flex: '0 0 auto' }); + flexContainer.addItem(bubbleChart, { flex: '0 0 auto' }); + flexContainer.addItem(polarChart, { flex: '0 0 auto' }); + flexContainer.addItem(radarChart, { flex: '0 0 auto' }); + + const flexWrapper = view.modelBuilder.loadingComponent().withItem(flexContainer).component(); + flexWrapper.loading = false; + + await view.initializeModel(flexWrapper); + } private registerSqlServicesModelView(): void { azdata.ui.registerModelViewProvider('sqlservices', async (view) => { diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 9cd09d54b11f..74b748ed6457 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -2086,6 +2086,430 @@ declare module 'azdata' { openFileBrowser(ownerUri: string, expandPath: string, fileFilters: string[], changeFilter: boolean, showFoldersOnly?: boolean): Thenable; } + //#region Chart component model types + + export type ChartType = 'bar' | 'bubble' | 'doughnut' | 'horizontalBar' | 'line' | 'pie' | 'polarArea' | 'radar' | 'scatter'; + + export interface ModelBuilder { + chart, TOptions extends ChartOptions>(): ComponentBuilder, ChartComponentProperties>; + } + + export interface ChartComponent, TOptions extends ChartOptions> extends Component, ChartComponentProperties { + onDidClick: vscode.Event; + } + + export interface ChartComponentProperties, TOptions extends ChartOptions> extends ComponentProperties { + /** + * Type of chart to build. Must match the ChartType parameter used to construct the chart. + */ + chartType: TChartType; // Necessary because all typing information from the generic parameters is lost after compilation + + /** + * Datasets and labels (if applicable) for the chart + */ + data: TData; + + /** + * Options for the chart configuration + */ + options?: TOptions; + } + + /** + * Base type for chart data + */ + export interface ChartData { + /** + * Never needs to be set or used. Only present for the TypeScript compiler to recognize the pairing between same-chart Data and Options types. + */ + // DevNote: + // This works because it gets compiled to (e.g.) `'bar' | undefined, forcing it to be associated with BarChartOptions + // and preventing it from being paired with PieChartOptions. + type?: TChartType; + } + + //#region Chart general data types + + export interface ChartDataEntryBase { + /** + * For Pie, Doughnut, Polar Area charts, it is the label associated with the data value. + * For Bar, Horizontal Bar, Line, Scatterplot, Bubble, and Radial, it is the label name for dataset. + */ + dataLabel: string; + /** + * Background color for chart elements. May be a name ('red'), hex ('#FFFFFF[77]), or RGB ('rgb(255, 255, 255[, 0.5])). + * Bracketed portions are optional for alpha/transparency. + */ + backgroundColor: string; + /** + * Border color for chart elements. May be a name ('red'), hex ('#FFFFFF[77]), or RGB ('rgb(255, 255, 255[, 0.5])). + * Bracketed portions are optional for alpha/transparency. + */ + borderColor?: string; + } + + export interface ChartDataEntry extends ChartDataEntryBase { + /** + * Value of one-dimensional data point + */ + value: Chart1DPoint | number; + } + + export interface ChartDataSet extends ChartDataEntryBase { + data: TVal[]; + } + + /** + * One-dimensional data point + */ + export interface Chart1DPoint { + /** + * Value for a one-dimensional data point, or the x-coordinate for a multi-dimensional data point + */ + x: number; + } + + /** + * Two-dimensional data point + */ + export interface Chart2DPoint extends Chart1DPoint { + /** + * Y-coordiate for a multi-dimensional data point + */ + y: number; + } + + /** + * Three-dimensional data point + */ + export interface Chart3DPoint extends Chart2DPoint { + /** + * Radius for a bubble data point, in pixels + */ + r: number; + } + + //#endregion + + //#region Chart general option types + + /** + * Base options for a chart + */ + export interface ChartOptions { + /** + * Never needs to be set or used. Only present for the TypeScript compiler to recognize the pairing between same-chart Data and Options types. + */ + // DevNote: + // This works because it gets compiled to (e.g.) `'bar' | undefined, forcing it to be associated with BarChartData + // and preventing it from being paired with PieChartData. + type?: TChartType; + + /** + * Title of the chart. Set to `undefined` to not display the title. + */ + chartTitle?: string; + + /** + * Whether to display the legend. Defaults to true. + */ + legendVisible?: boolean; + } + + /** + * Base options for scales + */ + export interface ScaleOptions { + /** + * Whether to begin the scale at zero + */ + beginAtZero?: boolean; + + /** + * Minimum value of the scale + */ + min?: number; + + /** + * Maxium value of the scale + */ + max?: number; + + /** + * Whether to add extra space between the scale and the chart + */ + offset?: boolean; + + /** + * Whether to stack charted values + */ + stacked?: boolean; + } + + //#endregion + + //#region Chart-specific types + + //#region Bar/Horizontal Bar charts + + export interface BarChartDataSet extends ChartDataSet { } + + export interface BarChartDataBase { + /** + * Array of datasets for the chart + */ + datasets: BarChartDataSet[]; + + /** + * Labels for the base axis. Only data that aligns with a label is shown. If there are fewer labels than data, then not all data is displayed; if there are more labels than data, then empty chart entries are appended + */ + labels: string[]; + } + + export interface BarChartOptionsBase { + /** + * Options for the scales + */ + scales?: { + /** + * Options for the X-axis + */ + x?: ScaleOptions; + + /** + * Options for the Y-axis + */ + y?: ScaleOptions; + } + } + + /** + * Data for a vertical bar chart + */ + export interface BarChartData extends ChartData<'bar'>, BarChartDataBase { } + + /** + * Options for a vertical bar chart + */ + export interface BarChartOptions extends ChartOptions<'bar'>, BarChartOptionsBase { } + + /** + * Data for a horizontal bar chart + */ + export interface HorizontalBarChartData extends ChartData<'horizontalBar'>, BarChartDataBase { } + + /** + * Options for a horizontal bar chart + */ + export interface HorizontalBarChartOptions extends ChartOptions<'horizontalBar'>, BarChartOptionsBase { } + + //#endregion + + //#region Line chart + + /** + * Data for a line chart + */ + export interface LineChartData extends ChartData<'line'>, BarChartDataBase { } + + /** + * Options for a line chart + */ + export interface LineChartOptions extends ChartOptions<'line'>, BarChartOptionsBase { + /** + * Which axis to use as the base, x or y; defaults to x + */ + indexAxis?: string; + + /** + * Bezier curve tension between points, 0 for straight lines. Recommended range: 0.0-1.0 + */ + tension?: number; + } + + //#endregion + + //#region Pie/Doughnut charts + + export interface PieChartDataBase { + /** + * Dataset for the chart + */ + dataset: ChartDataEntry[]; + } + + export interface PieChartOptionsBase { + circumference?: number; + /** + * Size of the cutout for a pie/doughnut chart, in pixels or percentage. Pie chart defaults to 0. Doughnut chart defaults to 50%. + */ + cutout?: number | string; + + /** + * Size of the outer radius for a pie/doughnut chart, in pixels or percentage of chart area + */ + radius?: number | string; + + /** + * Degrees of rotation to start drawing the first data entry from + */ + rotation?: number; + } + + /** + * Data for a Pie chart + */ + export interface PieChartData extends ChartData<'pie'>, PieChartDataBase { } + + /** + * Options for a Pie chart + */ + export interface PieChartOptions extends ChartOptions<'pie'>, PieChartOptionsBase { } + + /** + * Data for a Doughnut chart + */ + export interface DoughnutChartData extends ChartData<'doughnut'>, PieChartDataBase { } + + /** + * Options for a Doughnut chart + */ + export interface DoughnutChartOptions extends ChartOptions<'doughnut'>, PieChartOptionsBase { } + + //#endregion + + //#region Scatterplot + + export interface ScatterplotOptionBase { + /** + * Options for scales + */ + scales?: { + /** + * Options for the X-axis + */ + x?: ScaleOptions & { position?: 'left' | 'top' | 'right' | 'bottom' | 'center' }; + + /** + * Options for the Y-axis + */ + y?: ScaleOptions & { position?: 'left' | 'top' | 'right' | 'bottom' | 'center' }; + } + } + + /** + * Data for a scatter plot chart + */ + export interface ScatterplotData extends ChartData<'scatter'> { + /** + * Array of datasets for the chart + */ + datasets: ScatterplotDataSet[]; + } + + export interface ScatterplotDataSet extends ChartDataSet { } + + export interface ScatterplotOptions extends ChartOptions<'scatter'>, ScatterplotOptionBase { } + + //#endregion + + //#region Bubble chart + + /** + * Data for a bubble chart + */ + export interface BubbleChartData extends ChartData<'bubble'> { + /** + * Array of datasets for the chart + */ + datasets: BubbleChartDataSet[]; + } + + export interface BubbleChartDataSet extends ChartDataSet { } + + export interface BubbleChartOptions extends ChartOptions<'bubble'>, ScatterplotOptionBase { } + + //#endregion + + //#region Polar Area chart + + /** + * Data for a polar area chart + */ + export interface PolarAreaChartData extends ChartData<'polarArea'> { + /** + * Dataset for the chart + */ + dataset: ChartDataEntry[]; + } + + export interface PolarAreaChartOptions extends ChartOptions<'polarArea'> { + /** + * Whether to display data areas with circular edges. Defaults to true. + */ + circular?: boolean; + } + + //#endregion + + //#region Radar chart + + /** + * Data for a radar chart + */ + export interface RadarChartData extends ChartData<'radar'> { + /** + * Array of datasets for the chart + */ + datasets: BarChartDataSet[]; + /** + * Labels for the perimeter. Only data that aligns with a label is shown. If there are fewer labels than data, then not all data is displayed; if there are more labels than data, then empty chart entries are appended + */ + labels: string[]; + } + + export interface RadarChartOptions extends ChartOptions<'radar'> { + /** + * Options for scales + */ + scales?: { + /** + * Options for the radial axis + */ + r?: { + /** + * Angle to start the first data entry from. Defaults to 0 + */ + startAngle?: number; + + /** + * Value to start the radial axis from. Calculated if not set. + */ + beginAtZero?: boolean; + + /** + * Minimum value for the radial axis. Calculated if not set. + */ + min?: number; + + /** + * Maximum value for the radial axis. Calculated if not set. + */ + max?: number; + } + } + /** + * Bezier curve tension between points, 0 for straight lines. Recommended range: 0.0-1.0 + */ + tension?: number; + } + + //#endregion + + //#endregion + + //#endregion + export interface TableComponent { /** * Set active cell. diff --git a/src/sql/base/browser/ui/chart/chart.component.html b/src/sql/base/browser/ui/chart/chart.component.html new file mode 100644 index 000000000000..9bd6b6c68751 --- /dev/null +++ b/src/sql/base/browser/ui/chart/chart.component.html @@ -0,0 +1,5 @@ +
+
+ +
+
diff --git a/src/sql/base/browser/ui/chart/chart.component.ts b/src/sql/base/browser/ui/chart/chart.component.ts new file mode 100644 index 000000000000..68be9b96a6f0 --- /dev/null +++ b/src/sql/base/browser/ui/chart/chart.component.ts @@ -0,0 +1,366 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Component, Inject, forwardRef, ChangeDetectorRef } from '@angular/core'; +import * as chartjs from 'chart.js'; +import { mixin } from 'sql/base/common/objects'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as azdata from 'azdata'; +import { generateUuid } from 'vs/base/common/uuid'; + +export interface BarDataSet { + label: string; + data: number[]; + backgroundColor?: string[]; + borderColor?: string[]; +} + +@Component({ + selector: 'chart-component', + templateUrl: decodeURI(require.toUrl('./chart.component.html')) +}) +export class Chart, TOptions extends azdata.ChartOptions> extends Disposable { + private _type: TChartType; + private _data: chartjs.ChartData; + + private chart: chartjs.Chart; + private canvas: HTMLCanvasElement; + private chartCanvasId: string; + + /** + * Options in the form that Chart.js accepts + */ + private _options: chartjs.ChartOptions = { + events: ['click', 'keyup'], + responsive: true, + maintainAspectRatio: false + }; + + constructor( + @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef + ) { + chartjs.Chart.register(...chartjs.registerables); + super(); + + this.chartCanvasId = 'chart' + generateUuid(); + } + + ngAfterViewInit(): void { + + } + + /** + * Setter function for chart type + */ + public set type(val: TChartType) { + this._type = val; + + if (val === 'horizontalBar') { + // In Chart.js, horizontal bar charts are just bar charts with a different indexAxis set. + // The indexAxis gets set here, and the Chart.js type gets mapped at conversion time. + this._options = mixin({}, mixin(this._options, { indexAxis: 'y' })); + } + + this._changeRef.detectChanges(); + } + + /** + * Setter function for chart data + */ + public set data(val: TData) { + this._data = this.convertData(val); + + this.drawChart(); + } + + /** + * Setter function for chart options. + * Some options like responsiveness and maintainaspectratio are set by default and will be used even if no options are provided. + */ + public set options(val: TOptions) { + if (val === undefined) { + return; + } + + // mix in initial options + this._options = mixin({}, mixin(this._options, val)); + + // ...then set title and legend properties + if (val !== undefined) { + if (val.chartTitle) { // undefined results in hiding title + if (typeof val.chartTitle === 'string') { + this._options = mixin(this._options, { + plugins: { + title: { + text: val.chartTitle, + display: true + } + } + }); + } + } else { + this._options = mixin(this._options, { plugins: { title: { display: false } } }); + } + + if (val.legendVisible !== false) { // undefined defaults to true + this._options = mixin(this._options, { plugins: { legend: { display: true } } }); + } else { + this._options = mixin(this._options, { plugins: { legend: { display: false } } }); + } + } + + this.drawChart(); + } + + public set height(val: string | number) { + if (val && this.chart) { + (this.chart.canvas.parentNode as any).style.height = val; + } + } + + public set width(val: string | number) { + if (val && this.chart) { + (this.chart.canvas.parentNode as any).style.width = val; + } + } + + /** + * Function to draw the chart. + * If the chart is already present, a call to this will simply update the chart with new data values (if any). + * Else a new chart will be created. + */ + public drawChart() { + let canvas = document.getElementById(this.chartCanvasId) as HTMLCanvasElement; + this.canvas = canvas; + + if (this.chart) { + this.chart.data = this._data; + this.chart.update(); + } else { + this.chart = new chartjs.Chart(this.canvas.getContext("2d"), { + type: this.convertChartType(), + plugins: [plugin], + data: this._data, + options: this._options + }); + } + } + + private convertData(val: azdata.ChartData): chartjs.ChartData { + const result: chartjs.ChartData = { + datasets: [] + } + + switch (this._type) { + case 'bar': + case 'horizontalBar': // should've been replaced with 'bar' by this point, but inlcuded case here for safety + case 'line': + { + if (this.isBarOrLineChartData(val)) { + for (let set of val.datasets) { + result.datasets.push({ + data: set.data.map(entry => typeof entry === 'number' ? entry : entry.x), + backgroundColor: set.backgroundColor, + borderColor: set.borderColor, + label: set.dataLabel + }); + } + + result.labels = val.labels; + } + + break; + } + case 'pie': + case 'doughnut': + { + if (this.isPieOrDoughnutChartData(val)) { + result.datasets.push({ + data: val.dataset.map(entry => typeof entry.value === 'number' ? entry.value : entry.value.x), + backgroundColor: val.dataset.map(entry => entry.backgroundColor), + borderColor: val.dataset.map(entry => entry.borderColor) + }); + + result.labels = val.dataset.map(val => val.dataLabel); + } + + break; + } + case 'scatter': + { + if (this.isScatterplotData(val)) { + for (let set of val.datasets) { + result.datasets.push({ + data: set.data.map(entry => [entry.x, entry.y]), + backgroundColor: set.backgroundColor, + borderColor: set.borderColor, + label: set.dataLabel + }); + } + } + + break; + } + case 'bubble': + { + if (this.isBubbleChartData(val)) { + for (let set of val.datasets) { + result.datasets.push({ + data: set.data.map(entry => ({ x: entry.x, y: entry.y, r: entry.r })), + backgroundColor: set.backgroundColor, + borderColor: set.borderColor, + label: set.dataLabel + }); + } + } + + break; + } + case 'polarArea': + { + if (this.isPolarAreaChartData(val)) { + result.datasets.push({ + data: val.dataset.map(entry => typeof entry.value === 'number' ? entry.value : entry.value.x), + backgroundColor: val.dataset.map(entry => entry.backgroundColor), + borderColor: val.dataset.map(entry => entry.borderColor) + }); + + result.labels = val.dataset.map(val => val.dataLabel); + } + + break; + } + case 'radar': + { + if (this.isRadarChartData(val)) { + for (let set of val.datasets) { + result.datasets.push({ + data: set.data.map(entry => typeof entry === 'number' ? entry : entry.x), + backgroundColor: set.backgroundColor, + borderColor: set.borderColor, + label: set.dataLabel + }); + } + + result.labels = val.labels; + } + + break; + } + default: + throw new Error(`Unsupported chart type: '${this._type}'`); + } + + return result; + } + + private convertChartType(): chartjs.ChartType { + switch (this._type) { + case 'horizontalBar': // our 'horizontalBar' is just Chart.js's 'bar' with the indexAxis option set + return 'bar'; + default: // everything else matches up + return this._type; + } + } + + //#region Type predicates + + private isBarOrLineChartData(data: unknown): data is BarOrLineChartData { + return (data as BarOrLineChartData).datasets !== undefined + && (data as BarOrLineChartData).labels !== undefined; + } + + private isPieOrDoughnutChartData(data: unknown): data is PieOrDoughnutChartData { + return (data as PieOrDoughnutChartData).dataset !== undefined; + } + + private isScatterplotData(data: unknown): data is azdata.ScatterplotData { + return (data as azdata.ScatterplotData).datasets !== undefined; + } + + private isBubbleChartData(data: unknown): data is azdata.BubbleChartData { + return (data as azdata.BubbleChartData).datasets !== undefined; + } + + private isPolarAreaChartData(data: unknown): data is azdata.PolarAreaChartData { + return (data as azdata.PolarAreaChartData).dataset !== undefined; + } + + private isRadarChartData(data: unknown): data is azdata.RadarChartData { + return (data as azdata.RadarChartData).datasets !== undefined + && (data as azdata.RadarChartData).labels !== undefined; + } + + //endregion +} + +//#region Data compatibility groups + +type BarOrLineChartData = azdata.BarChartData | azdata.HorizontalBarChartData | azdata.LineChartData; +type PieOrDoughnutChartData = azdata.PieChartData | azdata.DoughnutChartData; + +//#endregion + +//#region Events + +const setActiveElements = function (chart, index) { + chart.setActiveElements([ + { + datasetIndex: 0, + index, + } + ]); + chart.update(); +}; + +const currentActiveElement = function (elements) { + if (elements.length) { + return elements[0].index; + } + return -1; +}; + +const dispatchClick = function (chart, point) { + const node = chart.canvas; + const rect = node.getBoundingClientRect(); + const event = new MouseEvent('click', { + clientX: rect.left + point.x, + clientY: rect.top + point.y, + cancelable: true, + bubbles: true + }); + node.dispatchEvent(event); +} + +const plugin = { + id: 'keyup', + defaults: { + events: ['keyup'] + }, + beforeEvent(chart, args, options) { + const event = args.event; + const code = event.native.code; + const activeElements = chart.getActiveElements(); + const tooltip = chart.tooltip; + if (code === 'ArrowRight') { + const pos = currentActiveElement(activeElements) + 1; + const index = pos === chart.data.datasets[0].data.length ? 0 : pos; + setActiveElements(chart, index); + setActiveElements(tooltip, index); + } else if (code === 'ArrowLeft') { + const pos = currentActiveElement(activeElements) - 1; + const index = pos < 0 ? chart.data.datasets[0].data.length - 1 : pos; + setActiveElements(chart, index); + setActiveElements(tooltip, index); + } else if (code === 'Enter' && activeElements.length) { + const el = activeElements[0]; + const meta = chart.getDatasetMeta(el.datasetIndex); + const data = meta.data[el.index]; + dispatchClick(chart, data); + } + return false; + } +}; + +//#endregion diff --git a/src/sql/base/browser/ui/chart/chart.module.ts b/src/sql/base/browser/ui/chart/chart.module.ts new file mode 100644 index 000000000000..16395131006e --- /dev/null +++ b/src/sql/base/browser/ui/chart/chart.module.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Chart } from 'sql/base/browser/ui/chart/chart.component'; + + +@NgModule({ + declarations: [ + Chart + ], + imports: [ + CommonModule + ], + exports: [Chart] +}) +export class ChartModule { } diff --git a/src/sql/platform/dashboard/browser/interfaces.ts b/src/sql/platform/dashboard/browser/interfaces.ts index 50a3fda8b4e8..d98a92158bc9 100644 --- a/src/sql/platform/dashboard/browser/interfaces.ts +++ b/src/sql/platform/dashboard/browser/interfaces.ts @@ -151,5 +151,6 @@ export enum ModelComponentTypes { PropertiesContainer, InfoBox, Slider, - ExecutionPlan + ExecutionPlan, + Chart } diff --git a/src/sql/workbench/api/common/extHostModelView.ts b/src/sql/workbench/api/common/extHostModelView.ts index 42a49ccb8d48..a6b33ad9ba78 100644 --- a/src/sql/workbench/api/common/extHostModelView.ts +++ b/src/sql/workbench/api/common/extHostModelView.ts @@ -290,6 +290,13 @@ class ModelBuilderImpl implements azdata.ModelBuilder { return builder; } + chart, TOptions extends azdata.ChartOptions>(): azdata.ComponentBuilder, azdata.ChartComponentProperties> { + let id = this.getNextComponentId(); + let builder: ComponentBuilderImpl, azdata.ChartComponentProperties> = this.getComponentBuilder(new ChartComponentWrapper(this._proxy, this._handle, id, this.logService), id); + this._componentBuilders.set(id, builder); + return builder; + } + getComponentBuilder(component: ComponentWrapper, id: string): ComponentBuilderImpl { let componentBuilder: ComponentBuilderImpl = new ComponentBuilderImpl(component); this._componentBuilders.set(id, componentBuilder); @@ -2273,6 +2280,44 @@ class GroupContainerComponentWrapper extends ComponentWrapper implements azdata. } } +class ChartComponentWrapper, TOptions extends azdata.ChartOptions> extends ComponentWrapper implements azdata.ChartComponent { + constructor(proxy: MainThreadModelViewShape, handle: number, id: string, logService: ILogService) { + super(proxy, handle, ModelComponentTypes.Chart, id, logService); + this.properties = {}; + + this._emitterMap.set(ComponentEventType.onDidClick, new Emitter()); + } + + public set chartType(v: TChartType) { + this.setProperty('chartType', v); + } + + public get chartType(): TChartType { + return this.properties['chartType']; + } + + public set data(v: TData) { + this.setProperty('data', v); + } + + public get data(): TData { + return this.properties['data']; + } + + public set options(v: TOptions) { + this.setProperty('options', v); + } + + public get options(): TOptions { + return this.properties['options']; + } + + public get onDidClick(): vscode.Event { + let emitter = this._emitterMap.get(ComponentEventType.onDidClick); + return emitter && emitter.event; + } +} + class ModelViewImpl extends Disposable implements azdata.ModelView { public onClosedEmitter = this._register(new Emitter()); diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 0186c9be612f..5f3a12ae4ce8 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -181,7 +181,8 @@ export enum ModelComponentTypes { PropertiesContainer, InfoBox, Slider, - ExecutionPlan + ExecutionPlan, + Chart } export enum ModelViewAction { diff --git a/src/sql/workbench/browser/modelComponents/chart.component.html b/src/sql/workbench/browser/modelComponents/chart.component.html new file mode 100644 index 000000000000..006a65db136b --- /dev/null +++ b/src/sql/workbench/browser/modelComponents/chart.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/sql/workbench/browser/modelComponents/chart.component.ts b/src/sql/workbench/browser/modelComponents/chart.component.ts new file mode 100644 index 000000000000..bc90d3cb7f03 --- /dev/null +++ b/src/sql/workbench/browser/modelComponents/chart.component.ts @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Component, Input, Inject, ChangeDetectorRef, forwardRef, OnDestroy, ElementRef, AfterViewInit, ViewChild } from '@angular/core'; + +import * as azdata from 'azdata'; +import { ComponentBase } from 'sql/workbench/browser/modelComponents/componentBase'; +import { IComponent, IComponentDescriptor, IModelStore } from 'sql/platform/dashboard/browser/interfaces'; +import { Chart } from 'sql/base/browser/ui/chart/chart.component'; +import { ILogService } from 'vs/platform/log/common/log'; + +@Component({ + selector: 'modelview-chart', + templateUrl: decodeURI(require.toUrl('./chart.component.html')) +}) + +export default class ChartComponent, TOptions extends azdata.ChartOptions> extends ComponentBase> implements IComponent, OnDestroy, AfterViewInit { + + @Input() descriptor: IComponentDescriptor; + @Input() modelStore: IModelStore; + + @ViewChild(Chart) private _chart: Chart; + + constructor( + @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef, + @Inject(forwardRef(() => ElementRef)) el: ElementRef, + @Inject(ILogService) logService: ILogService) { + super(changeRef, el, logService); + } + + ngAfterViewInit(): void { + this.baseInit(); + } + + override ngOnDestroy(): void { + this.baseDestroy(); + } + + public override setProperties(properties: { [key: string]: any; }): void { + super.setProperties(properties); + + // chartType must be set before data because it's necessary for the draw function that triggers when setting data + + if (this.chartType) { + this._chart.type = this.chartType; + } + + if (this.data) { + this._chart.data = this.data; + } + + if (this.options) { + this._chart.options = this.options; + } + + if (this.height) { + this._chart.height = this.height; + } + + if (this.width) { + this._chart.width = this.width; + } + } + + public get chartType(): TChartType { + return this.getProperties().chartType; + } + + public get data(): TData { + return this.getProperties().data; + } + + public get options(): TOptions | undefined { + return this.getProperties().options; + } + + public setLayout(layout: any): void { + this.layout(); + } +} diff --git a/src/sql/workbench/contrib/dashboard/browser/dashboard.module.ts b/src/sql/workbench/contrib/dashboard/browser/dashboard.module.ts index b25d5434707c..29414fa80248 100644 --- a/src/sql/workbench/contrib/dashboard/browser/dashboard.module.ts +++ b/src/sql/workbench/contrib/dashboard/browser/dashboard.module.ts @@ -89,6 +89,7 @@ import { IInstantiationService, _util } from 'vs/platform/instantiation/common/i import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { PropertiesContainerModule } from 'sql/base/browser/ui/propertiesContainer/propertiesContainer.module'; import { LoadingSpinnerModule } from 'sql/base/browser/ui/loadingSpinner/loadingSpinner.module'; +import { ChartModule } from 'sql/base/browser/ui/chart/chart.module'; const widgetComponents = [ @@ -143,7 +144,8 @@ export const DashboardModule = (params, selector: string, instantiationService: PanelModule, ScrollableModule, PropertiesContainerModule, - LoadingSpinnerModule + LoadingSpinnerModule, + ChartModule ], providers: [ { provide: APP_BASE_HREF, useValue: '/' }, diff --git a/src/sql/workbench/contrib/modelView/browser/components.contribution.ts b/src/sql/workbench/contrib/modelView/browser/components.contribution.ts index 316b6e810af5..07f93044a591 100644 --- a/src/sql/workbench/contrib/modelView/browser/components.contribution.ts +++ b/src/sql/workbench/contrib/modelView/browser/components.contribution.ts @@ -37,6 +37,7 @@ import ListViewComponent from 'sql/workbench/browser/modelComponents/listView.co import InfoBoxComponent from 'sql/workbench/browser/modelComponents/infoBox.component'; import SliderComponent from 'sql/workbench/browser/modelComponents/slider.component'; import ExecutionPlanComponent from 'sql/workbench/browser/modelComponents/executionPlan.component'; +import ChartComponent from 'sql/workbench/browser/modelComponents/chart.component'; export const DIV_CONTAINER = 'div-container'; registerComponentType(DIV_CONTAINER, ModelComponentTypes.DivContainer, DivContainer); @@ -134,3 +135,6 @@ registerComponentType(SLIDER_COMPONENT, ModelComponentTypes.Slider, SliderCompon export const EXECUTION_PLAN_COMPONENT = 'executionplan-component'; registerComponentType(EXECUTION_PLAN_COMPONENT, ModelComponentTypes.ExecutionPlan, ExecutionPlanComponent); + +export const CHART_COMPONENT = 'chart-component'; +registerComponentType(CHART_COMPONENT, ModelComponentTypes.Chart, ChartComponent); \ No newline at end of file diff --git a/src/sql/workbench/services/dialog/browser/dialog.module.ts b/src/sql/workbench/services/dialog/browser/dialog.module.ts index 636a46f6134a..5591cb16a3f9 100644 --- a/src/sql/workbench/services/dialog/browser/dialog.module.ts +++ b/src/sql/workbench/services/dialog/browser/dialog.module.ts @@ -28,6 +28,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IBootstrapParams, ISelector } from 'sql/workbench/services/bootstrap/common/bootstrapParams'; import { PanelModule } from 'sql/base/browser/ui/panel/panel.module'; import { PropertiesContainerModule } from 'sql/base/browser/ui/propertiesContainer/propertiesContainer.module'; +import { ChartModule } from 'sql/base/browser/ui/chart/chart.module'; export const DialogModule = (params: IBootstrapParams, selector: string, instantiationService: IInstantiationService): any => { @@ -53,7 +54,8 @@ export const DialogModule = (params: IBootstrapParams, selector: string, instant CommonModule, BrowserModule, PanelModule, - PropertiesContainerModule + PropertiesContainerModule, + ChartModule ], providers: [ { provide: APP_BASE_HREF, useValue: '/' },