mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
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:
424
src/sql/azdata.proposed.d.ts
vendored
424
src/sql/azdata.proposed.d.ts
vendored
@@ -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.
|
||||
|
||||
5
src/sql/base/browser/ui/chart/chart.component.html
Normal file
5
src/sql/base/browser/ui/chart/chart.component.html
Normal 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>
|
||||
366
src/sql/base/browser/ui/chart/chart.component.ts
Normal file
366
src/sql/base/browser/ui/chart/chart.component.ts
Normal 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
|
||||
19
src/sql/base/browser/ui/chart/chart.module.ts
Normal file
19
src/sql/base/browser/ui/chart/chart.module.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
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 { }
|
||||
@@ -151,5 +151,6 @@ export enum ModelComponentTypes {
|
||||
PropertiesContainer,
|
||||
InfoBox,
|
||||
Slider,
|
||||
ExecutionPlan
|
||||
ExecutionPlan,
|
||||
Chart
|
||||
}
|
||||
|
||||
@@ -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>());
|
||||
|
||||
@@ -181,7 +181,8 @@ export enum ModelComponentTypes {
|
||||
PropertiesContainer,
|
||||
InfoBox,
|
||||
Slider,
|
||||
ExecutionPlan
|
||||
ExecutionPlan,
|
||||
Chart
|
||||
}
|
||||
|
||||
export enum ModelViewAction {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<div style="display: block; width: 100%; height: 100%; position: relative">
|
||||
<chart-component></chart-component>
|
||||
</div>
|
||||
81
src/sql/workbench/browser/modelComponents/chart.component.ts
Normal file
81
src/sql/workbench/browser/modelComponents/chart.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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: '/' },
|
||||
|
||||
@@ -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);
|
||||
@@ -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: '/' },
|
||||
|
||||
Reference in New Issue
Block a user