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 <deepaksaini@microsoft.com>
Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
Co-authored-by: Aasim Khan <aaskhan@microsoft.com>
Co-authored-by: Deepak Saini <deepak.saini1996@gmail.com>
This commit is contained in:
Benjin Dubishar
2023-09-13 20:11:09 -07:00
committed by GitHub
parent f7ac504a6c
commit d9b5d71148
21 changed files with 1419 additions and 8 deletions

View File

@@ -288,6 +288,7 @@ export function createViewContext(): ViewTestContext {
modelBuilder: {
listView: undefined!,
radioCardGroup: undefined!,
chart: undefined!,
navContainer: undefined!,
divContainer: () => divBuilder,
flexContainer: () => flexBuilder,

View File

@@ -597,6 +597,9 @@ export class MockModelBuilder implements azdata.ModelBuilder {
listView(): azdata.ComponentBuilder<azdata.ListViewComponent, azdata.ListViewComponentProperties> {
throw new Error('Method not implemented.');
}
chart<TChartType extends azdata.ChartType, TData extends azdata.ChartData<TChartType>, TOptions extends azdata.ChartOptions<TChartType>>(): azdata.ComponentBuilder<azdata.ChartComponent<TChartType, TData, TOptions>, azdata.ChartComponentProperties<TChartType, TData, TOptions>> {
throw new Error('Method not implemented.');
}
slider(): azdata.ComponentBuilder<azdata.SliderComponent, azdata.SliderComponentProperties> {
throw new Error('Method not implemented.');
}

View File

@@ -234,6 +234,7 @@ export function createViewContext(): ViewTestContext {
modelBuilder: {
listView: undefined!,
radioCardGroup: undefined!,
chart: undefined!,
navContainer: undefined!,
divContainer: () => divBuilder,
flexContainer: () => flexBuilder,

View File

@@ -274,6 +274,7 @@ describe('Manage Package Dialog', () => {
modelBuilder: {
listView: undefined!,
radioCardGroup: undefined!,
chart: undefined!,
navContainer: undefined!,
divContainer: undefined!,
flexContainer: () => flexBuilder,

View File

@@ -72,6 +72,7 @@ export function createViewContext(): TestContext {
modelBuilder: {
listView: undefined!,
radioCardGroup: undefined!,
chart: undefined!,
navContainer: undefined!,
divContainer: undefined!,
flexContainer: () => flexBuilder,

View File

@@ -330,6 +330,7 @@ export function createViewContext(): ViewTestContext {
modelBuilder: {
listView: undefined!,
radioCardGroup: undefined!,
chart: undefined!,
navContainer: undefined!,
divContainer: () => divBuilder,
flexContainer: () => flexBuilder,

View File

@@ -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

View File

@@ -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'
};

View File

@@ -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<void> {
const barChart = view.modelBuilder.chart<'bar', azdata.BarChartData, azdata.BarChartOptions>()
.withProps({
chartType: 'bar',
data: chartExamples.barData,
options: chartExamples.barOptions,
width: '500px',
height: '300px'
}).component();
//#region Registrations
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();
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) => {

View File

@@ -2086,6 +2086,430 @@ declare module 'azdata' {
openFileBrowser(ownerUri: string, expandPath: string, fileFilters: string[], changeFilter: boolean, showFoldersOnly?: boolean): Thenable<boolean>;
}
//#region Chart component model types
export type ChartType = 'bar' | 'bubble' | 'doughnut' | 'horizontalBar' | 'line' | 'pie' | 'polarArea' | 'radar' | 'scatter';
export interface ModelBuilder {
chart<TChartType extends ChartType, TData extends ChartData<TChartType>, TOptions extends ChartOptions<TChartType>>(): ComponentBuilder<ChartComponent<TChartType, TData, TOptions>, ChartComponentProperties<TChartType, TData, TOptions>>;
}
export interface ChartComponent<TChartType extends ChartType, TData extends ChartData<TChartType>, TOptions extends ChartOptions<TChartType>> extends Component, ChartComponentProperties<TChartType, TData, TOptions> {
onDidClick: vscode.Event<any>;
}
export interface ChartComponentProperties<TChartType extends ChartType, TData extends ChartData<TChartType>, TOptions extends ChartOptions<TChartType>> 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<TChartType extends ChartType> {
/**
* 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<TVal extends Chart1DPoint | number> 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<TChartType extends ChartType> {
/**
* 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<Chart1DPoint | number> { }
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<Chart2DPoint> { }
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<Chart3DPoint> { }
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.

View File

@@ -0,0 +1,5 @@
<div style="display: block; width: 100%; height: 100%; position: relative;">
<div class="chart-container">
<canvas id={{chartCanvasId}} tabindex="0"></canvas>
</div>
</div>

View File

@@ -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<TChartType extends azdata.ChartType, TData extends azdata.ChartData<TChartType>, TOptions extends azdata.ChartOptions<TChartType>> 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<TChartType>): 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

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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 { }

View File

@@ -151,5 +151,6 @@ export enum ModelComponentTypes {
PropertiesContainer,
InfoBox,
Slider,
ExecutionPlan
ExecutionPlan,
Chart
}

View File

@@ -290,6 +290,13 @@ class ModelBuilderImpl implements azdata.ModelBuilder {
return builder;
}
chart<TChartType extends azdata.ChartType, TData extends azdata.ChartData<TChartType>, TOptions extends azdata.ChartOptions<TChartType>>(): azdata.ComponentBuilder<azdata.ChartComponent<TChartType, TData, TOptions>, azdata.ChartComponentProperties<TChartType, TData, TOptions>> {
let id = this.getNextComponentId();
let builder: ComponentBuilderImpl<azdata.ChartComponent<TChartType, TData, TOptions>, azdata.ChartComponentProperties<TChartType, TData, TOptions>> = this.getComponentBuilder(new ChartComponentWrapper<TChartType, TData, TOptions>(this._proxy, this._handle, id, this.logService), id);
this._componentBuilders.set(id, builder);
return builder;
}
getComponentBuilder<T extends azdata.Component, TPropertyBag extends azdata.ComponentProperties>(component: ComponentWrapper, id: string): ComponentBuilderImpl<T, TPropertyBag> {
let componentBuilder: ComponentBuilderImpl<T, TPropertyBag> = new ComponentBuilderImpl<T, TPropertyBag>(component);
this._componentBuilders.set(id, componentBuilder);
@@ -2273,6 +2280,44 @@ class GroupContainerComponentWrapper extends ComponentWrapper implements azdata.
}
}
class ChartComponentWrapper<TChartType extends azdata.ChartType, TData extends azdata.ChartData<TChartType>, TOptions extends azdata.ChartOptions<TChartType>> extends ComponentWrapper implements azdata.ChartComponent<TChartType, TData, TOptions> {
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<any>());
}
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<any> {
let emitter = this._emitterMap.get(ComponentEventType.onDidClick);
return emitter && emitter.event;
}
}
class ModelViewImpl extends Disposable implements azdata.ModelView {
public onClosedEmitter = this._register(new Emitter<any>());

View File

@@ -181,7 +181,8 @@ export enum ModelComponentTypes {
PropertiesContainer,
InfoBox,
Slider,
ExecutionPlan
ExecutionPlan,
Chart
}
export enum ModelViewAction {

View File

@@ -0,0 +1,3 @@
<div style="display: block; width: 100%; height: 100%; position: relative">
<chart-component></chart-component>
</div>

View File

@@ -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<TChartType extends azdata.ChartType, TData extends azdata.ChartData<TChartType>, TOptions extends azdata.ChartOptions<TChartType>> extends ComponentBase<azdata.ChartComponentProperties<TChartType, TData, TOptions>> implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
@ViewChild(Chart) private _chart: Chart<TChartType, TData, TOptions>;
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();
}
}

View File

@@ -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: '/' },

View File

@@ -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);

View File

@@ -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: '/' },