Example T1053403
Visible to All Users

Dashboard for Angular - Custom Item Gallery

The example contains the source code of the most requested custom items you can use in your Web Dashboard application. Use the custom items from this example as they are, or modify them according to your needs. In this Web Dashboard application, you can add custom items from the Custom Items group in the Toolbox:

toolbox

This example uses a client-server architecture. The server (backend) project communicates with the client (frontend) application that includes all the necessary styles, scripts and HTML templates. Note that the script version on the client must match the version of libraries on the server.

Files to Review

Quick Start

Server

Run the following command in the asp-net-core-server folder:

Code
dotnet run

The server starts at http://localhost:5000 and the client gets data from http://localhost:5000/api/dashboard. To debug the server, run the asp-net-core-server application in Visual Studio and change the client's endpoint property according to the listening port: https://localhost:44371/api/dashboard.

See the following section for information on how to install NuGet packages from the DevExpress NuGet feed: Install DevExpress Controls Using NuGet Packages.

This server allows CORS requests from all origins with any scheme (http or https). This default configuration is insecure: any website can make cross-origin requests to the app. We recommend that you specify the client application's URL to prohibit other clients from accessing sensitive information stored on the server. Learn more: Cross-Origin Resource Sharing (CORS)

Client

In the dashboard-react-app folder, run the following commands:

Code
npm install npm start

Open http://localhost:4200/ in your browser to see the Web Dashboard application.

Country Sales Dashboard

The dashboard displays product sales for the selected category. Use the Country parameter to filter data by country. Select a category on the Polar Chart to show sales by products from this category in the table.

This dashboard contains the following custom items:

Simple Table

View Script: simple-table-item.ts

A custom Simple Table item renders data from the measure / dimensions as an HTML table. You can use the Simple Table as a detail item along with the Master-Filtering feature. This custom item supports the following settings that you can configure in the Web Dashboard UI:

simple-table-item

  • Show Headers - Specifies whether to show the field headers in the table. The default value is Auto.
  • Text Color - Allows you to change the text color. The default value is Dark.

Funnel D3 Chart Item

View Script: funnel-d3-item.ts

A custom Funnel D3 Chart item renders a funnel chart using the D3Funnel JS library. This custom item supports the following settings that you can configure in the Web Dashboard UI:

funnel-d3-item

  • Fill Type - Specifies the funnel chart's solid or gradient fill type.
  • Curved - Specifies whether the funnel is curved.
  • Dynamic Height - Specifies whether the height of blocks are proportional to their weight.
  • Pinch Count - Specifies how many blocks to pinch at the bottom to create a funnel "neck".

Polar Chart Item

View Script: polar-chart-item.ts

A custom Polar Chart item that allows you to use the dxPolarChart DevExtreme widget in your dashboards. This item supports the following settings that you can configure in the Web Dashboard UI:

polar-chart-item

  • Display Labels - Specifies whether to show point labels.

Parameter Item

View Script: parameter-item.ts

A custom Parameter item renders dashboard parameter dialog content inside the dashboard layout, and allows you to edit and submit parameter values. This item supports the following settings that you can configure in the Web Dashboard UI:

parameter-item

  • Show Headers - Specifies whether to show headers in the parameters table.
  • Show Parameter Name - Specifies whether to show the first column with parameter names.
  • Automatic Updates - Specifies whether a parameter item is updated automatically. When enabled, this option hides the 'Submit' and 'Reset' buttons.

Country Info Dashboard

The dashboard displays information from Wikipedia for the selected country.

This dashboard contains the following custom items:

Online Map

View Script: online-map-item.ts

A custom Online Map item allows you to place callouts on Google or Bing maps using geographical coordinates. The dxMap is used as an underlying UI component. This custom item supports the following settings that you can configure in the Web Dashboard UI:

online-map-item

  • Provider - Specifies whether to show Google or Bing maps.
  • Type - Specifies the map type. You can choose between RoadMap, Satellite or Hybrid.
  • Display Mode - Specifies whether to show markers or routes.

Web Page

View Script: webpage-item.ts

A custom Web Page item displays a single web page or a set of pages. You can use the Web Page as a detail item along with the Master-Filtering feature. The content is rendered inside the Inline Frame element (<iframe>). This custom item supports the following setting that you can configure in the Web Dashboard UI:

web-page-item

  • URL - Specifies a web page URL. You can set a single page as well as a set of pages (e.g., 'https://en.wikipedia.org/wiki/{0}'). If you add a dimension and specify a placeholder, the data source field returns strings that will be inserted in the position of the {0} placeholder. Thus, the Web Page item joins the specified URL with the current dimension value and displays the page located by this address.

Tasks Dashboard

The dashboard displays tasks. Select the task to display detailed information in the Grid.

This dashboard contains the following custom item:

Gannt Item

View Script: gantt-item.ts

A custom Gannt item displays the task flow and dependencies between tasks. This item uses dxGantt as an underlying UI component.

gantt-item

Departments Dashboard

The dashboard displays departmental data. Use the custom Tree View item to filter detailed information in the Grid.

This dashboard contains the following custom item:

Hierarchical Tree View

View Script: hierarchical-tree-view-item.ts

A custom Tree View item can display hierarchical data. This item uses dxTreeView as an underlying UI component.

hierachical-tree-view

License

These extensions are distributed under the MIT license (free and open-source), but can only be used with a commercial DevExpress Dashboard software product. You can review the license terms or download a free trial version of the Dashboard suite at DevExpress.com.

Documentation

More Examples

Does this example address your development requirements/objectives?

(you will be redirected to DevExpress.com to submit your response)

Example Code

dashboard-angular-app/src/app/extensions/simple-table-item.ts
TypeScript
import * as Model from 'devexpress-dashboard/model'; import { FormItemTemplates } from 'devexpress-dashboard/designer'; import { ICustomItemExtension, CustomItemViewer } from 'devexpress-dashboard/common'; import { ICustomItemMetaData } from 'devexpress-dashboard/model/items/custom-item/meta'; const SIMPLE_TABLE_EXTENSION_NAME = 'CustomItemSimpleTable'; const svgIcon = `<?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="` + SIMPLE_TABLE_EXTENSION_NAME + `" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <path class="dx-dashboard-contrast-icon" d="M21,2H3C2.5,2,2,2.5,2,3v18c0,0.5,0.5,1,1,1h18c0.5,0,1-0.5,1-1V3 C22,2.5,21.5,2,21,2z M14,4v4h-4V4H14z M10,10h4v4h-4V10z M4,4h4v4H4V4z M4,10h4v4H4V10z M4,20v-4h4v4H4z M10,20v-4h4v4H10z M20,20 h-4v-4h4V20z M20,14h-4v-4h4V14z M20,8h-4V4h4V8z"/> </svg>`; const simpleTableMeta: ICustomItemMetaData = { // A collection of custom data bindings that are available in the Web Dashboard UI. bindings: [{ // A unique name of the data binding. propertyName: 'customDimensions', // A type of the data item(-s). dataItemType: 'Dimension', // Specifies whether this binding is a collection or a single value. array: true, // A caption of the data binding. displayName: "Custom Dimensions", emptyPlaceholder: 'Set Dimensions', selectedPlaceholder: "Configure Dimensions" }, { propertyName: 'customMeasure', dataItemType: 'Measure', array: false, displayName: "Custom Measure", emptyPlaceholder: 'Set Measure', selectedPlaceholder: "Configure Measure" }], customProperties: [{ ownerType: Model.CustomItem, propertyName: 'showHeaders', valueType: 'string', defaultValue: 'Auto', }], optionsPanelSections: [{ title: "Custom Options", items: [{ dataField: 'showHeaders', template: FormItemTemplates.buttonGroup, editorOptions: { items: [{ text: 'Auto' }, { text: 'Off' }, { text: 'On' }] } }] }], icon: SIMPLE_TABLE_EXTENSION_NAME, title: "Simple Table" }; export class SimpleTableItemExtension implements ICustomItemExtension { name = SIMPLE_TABLE_EXTENSION_NAME; metaData = simpleTableMeta; constructor(dashboardControl: any) { dashboardControl.registerIcon(svgIcon); } public createViewerItem = (model: any, element: any, content: any) => { return new SimpleTableItem(model, element, content); } } export class SimpleTableItem extends CustomItemViewer { private table?: HTMLTableElement; constructor(model: any, container: any, options: any) { super(model, container, options); } override renderContent(element: HTMLElement, changeExisting: boolean, afterRenderCallback?: any) { // The changeExisting flag indicates whether to update a custom item content or // render it from scratch when any changes exist (true to update content; otherwise, false). if (!changeExisting) { while (element.firstChild) element.removeChild(element.firstChild); element.style.overflow = 'auto'; this.table = <HTMLTableElement>(document.createElement('table')); this.table.setAttribute('cellpadding', '0'); this.table.setAttribute('cellspacing', '0'); this.table.setAttribute('border', '1'); element.append(this.table); } this.update(<string>this.getPropertyValue('showHeaders')); } update(mode: string) { while (this.table?.firstChild) this.table?.removeChild(this.table?.firstChild); if(mode != 'Off') { let bindingValues = this.getBindingValue('customDimensions').concat(this.getBindingValue('customMeasure')); this.addTableRow(bindingValues.map(function(item) { return item.displayName(); }), true); } // Iterates data rows for a custom item. Use the getValue and getDisplayText properties to get a value or a display name of the data row, respectively. this.iterateData(rowDataObject => { let valueTexts = rowDataObject.getDisplayText('customDimensions').concat(rowDataObject.getDisplayText('customMeasure')); this.addTableRow(valueTexts, false); }); } addTableRow(texts: string[], isHeader: boolean) { let row: HTMLTableRowElement = <HTMLTableRowElement>this.table?.createTHead().insertRow(); for (let text of texts) { let cell = isHeader ? row.appendChild(document.createElement("th")) : row.insertCell(); cell.style.padding = '3px'; cell.textContent = text; } } }
dashboard-angular-app/src/app/extensions/polar-chart-item.ts
TypeScript
import * as Model from 'devexpress-dashboard/model'; import { ICustomItemExtension, CustomItemViewer, CustomItemExportInfo } from 'devexpress-dashboard/common'; import { ICustomItemMetaData } from 'devexpress-dashboard/model/items/custom-item/meta'; import dxPolarChart from 'devextreme/viz/polar_chart'; const POLAR_CHART_EXTENSION_NAME = 'PolarChart'; const svgIcon = `<?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="` + POLAR_CHART_EXTENSION_NAME + `" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <path class="dx-dashboard-contrast-icon" d="M12,1C5.9,1,1,5.9,1,12s4.9,11,11,11s11-4.9,11-11S18.1,1,12,1z M12,21c-5,0-9-4-9-9s4-9,9-9s9,4,9,9S17,21,12,21z" /> <path class="dx-dashboard-accent-icon" d="M17,10c-0.6,0-1.1,0.2-1.5,0.4L13.8,9C13.9,8.7,14,8.4,14,8c0-1.7-1.3-3-3-3S8,6.3,8,8c0,1,0.5,2,1.3,2.5L8.7,13C7.2,13.2,6,14.4,6,16c0,1.7,1.3,3,3,3s3-1.3,3-3c0,0,0,0,0-0.1l2.7-1c0.6,0.7,1.4,1.1,2.3,1.1c1.7,0,3-1.3,3-3S18.7,10,17,10z M9,17c-0.6,0-1-0.4-1-1s0.4-1,1-1s1,0.4,1,1S9.6,17,9,17z M10,8c0-0.6,0.4-1,1-1s1,0.4,1,1s-0.4,1-1,1S10,8.6,10,8zM14,13.1l-2.7,1c-0.2-0.2-0.4-0.4-0.6-0.6l0.6-2.5c0.4,0,0.9-0.2,1.2-0.4l1.7,1.4C14.1,12.3,14,12.6,14,13.1C14,13,14,13,14,13.1zM17,14c-0.6,0-1-0.4-1-1s0.4-1,1-1s1,0.4,1,1S17.6,14,17,14z" /> </svg > `; const polarMeta: ICustomItemMetaData = { bindings: [{ propertyName: 'measureValue', dataItemType: 'Measure', displayName: 'Value', array: true, emptyPlaceholder: 'Set Value', selectedPlaceholder: 'Configure Value' }, { propertyName: 'dimensionValue', dataItemType: 'Dimension', displayName: 'Argument', array: false, enableColoring: true, enableInteractivity: true, emptyPlaceholder: 'Set Argument', selectedPlaceholder: 'Configure Argument' }], interactivity: { filter: true }, customProperties: [{ ownerType: Model.CustomItem, propertyName: 'labelVisibleProperty', valueType: 'boolean', defaultValue: true }], optionsPanelSections: [{ title: 'Labels', items: [{ dataField: 'labelVisibleProperty', label: { text: 'Display labels' } }] }], icon: POLAR_CHART_EXTENSION_NAME, title: 'Polar Chart' }; export class PolarChartItemExtension implements ICustomItemExtension { name = POLAR_CHART_EXTENSION_NAME; metaData = polarMeta; constructor(dashboardControl: any) { dashboardControl.registerIcon(svgIcon); } public createViewerItem = (model: any, element: any, content: any) => { return new PolarChartItem(model, element, content); } } export class PolarChartItem extends CustomItemViewer { dxPolarWidget?: dxPolarChart; dxPolarWidgetSettings?: any; exportingImageData?: string; constructor(model: any, container: any, options: any) { super(model, container, options); } _getDataSource() { let data: any[] = []; if (this.getBindingValue('measureValue').length > 0) { this.iterateData(dataRow => { let dataItem = <any>{ arg: dataRow.getValue('dimensionValue')[0] || "", color: dataRow.getColor()[0], clientDataRow: dataRow }; let measureValues = dataRow.getValue('measureValue'); for (let i = 0; i < measureValues.length; i++) { dataItem["measureValue" + i] = measureValues[i]; } data.push(dataItem); }); } return data; } _getDxPolarWidgetSettings() { let series: any[] = []; let dataSource = this._getDataSource(); let measureValueBindings = this.getBindingValue('measureValue'); for (let i = 0; i < measureValueBindings.length; i++) { series.push({ valueField: "measureValue" + i, name: measureValueBindings[i].displayName() }); } return { dataSource: dataSource, series: series, useSpiderWeb: true, resolveLabelOverlapping: "hide", pointSelectionMode: "multiple", commonSeriesSettings: { type: "line", label: { visible: <boolean>this.getPropertyValue("labelVisibleProperty") } }, "export": { enabled: false }, tooltip: { enabled: false }, onPointClick: (e: any) => { let point = e.target; this.setMasterFilter(point.data.clientDataRow); }, onDrawn: (e: any) => { this.convertSVGtoPNG(e.component.svg(), e.element.clientWidth, e.element.clientHeight).then((data: string) => { this.exportingImageData = data; }); } }; } override renderContent(element: HTMLElement, changeExisting: boolean, afterRenderCallback?: any) { if (!changeExisting) { while(element.firstChild) element.removeChild(element.firstChild); this.dxPolarWidget = new dxPolarChart(element, <any>this._getDxPolarWidgetSettings()); } else { this.dxPolarWidget?.option(<any>this._getDxPolarWidgetSettings()); } this._updateSelection(); } override setSelection(values: Array<Array<any>>): void { super.setSelection(values); this._updateSelection(); } _updateSelection() { let series = this.dxPolarWidget?.getAllSeries(); if (series) { for (let i = 0; i < series.length; i++) { let points = series[i].getAllPoints() for (let j = 0; j < points.length; j++) { if (this.isSelected(points[j].data.clientDataRow)) points[j].select(); else points[j].clearSelection(); } } } } override clearSelection(): void { super.clearSelection(); this.dxPolarWidget?.clearSelection(); } override setSize(width: number, height: number): void { super.setSize(width, height); this.dxPolarWidget?.render(); } override allowExportSingleItem(): boolean { return true; } override getExportInfo(): CustomItemExportInfo { return { image: this.exportingImageData }; } convertSVGtoPNG(svgString: string, width: number, height: number): PromiseLike<string> { return new Promise(function (resolve, reject) { try { const encodedData = 'data:image/svg+xml;base64,' + window.btoa(window['unescape'](encodeURIComponent(svgString))); var image = new Image(); var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; image.onload = () => { canvas.getContext('2d').drawImage(image, 0, 0); resolve(canvas.toDataURL().replace('data:image/png;base64,', '')); }; image.src = encodedData; } catch (err) { reject('Failed to convert SVG to PNG: ' + err); } }); } }
dashboard-angular-app/src/app/extensions/parameter-item.ts
TypeScript
import * as Model from 'devexpress-dashboard/model'; import { ICustomItemExtension, CustomItemViewer } from 'devexpress-dashboard/common'; import { ICustomItemMetaData } from 'devexpress-dashboard/model/items/custom-item/meta'; import { FormItemTemplates } from 'devexpress-dashboard/designer'; import dxButton from 'devextreme/ui/button'; const PARAMETER_EXTENSION_NAME = 'ParameterItem'; const svgIcon = `<?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="` + PARAMETER_EXTENSION_NAME + `" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <g class="st0"> <path class="dx-dashboard-contrast-icon" d="M6,12c0.4,0,0.7,0.1,1,0.2V5c0-0.6-0.4-1-1-1S5,4.4,5,5v7.2 C5.3,12.1,5.6,12,6,12z M6,18c-0.4,0-0.7-0.1-1-0.2V19c0,0.6,0.4,1,1,1s1-0.4,1-1v-1.2C6.7,17.9,6.4,18,6,18z M12,6 c0.4,0,0.7,0.1,1,0.2V5c0-0.6-0.4-1-1-1s-1,0.4-1,1v1.2C11.3,6.1,11.6,6,12,6z M12,12c-0.4,0-0.7-0.1-1-0.2V19c0,0.6,0.4,1,1,1 s1-0.4,1-1v-7.2C12.7,11.9,12.4,12,12,12z M18,17c-0.4,0-0.7-0.1-1-0.2V19c0,0.6,0.4,1,1,1s1-0.4,1-1v-2.2C18.7,16.9,18.4,17,18,17 z M18,11c0.4,0,0.7,0.1,1,0.2V5c0-0.6-0.4-1-1-1s-1,0.4-1,1v6.2C17.3,11.1,17.6,11,18,11z"/> </g> <path class="dx-dashboard-accent-icon" d="M6,12c-1.7,0-3,1.3-3,3s1.3,3,3,3s3-1.3,3-3S7.7,12,6,12z M6,16c-0.6,0-1-0.4-1-1 s0.4-1,1-1s1,0.4,1,1S6.6,16,6,16z M12,6c-1.7,0-3,1.3-3,3s1.3,3,3,3s3-1.3,3-3S13.7,6,12,6z M12,10c-0.6,0-1-0.4-1-1s0.4-1,1-1 s1,0.4,1,1S12.6,10,12,10z M18,11c-1.7,0-3,1.3-3,3s1.3,3,3,3s3-1.3,3-3S19.7,11,18,11z M18,15c-0.6,0-1-0.4-1-1s0.4-1,1-1 s1,0.4,1,1S18.6,15,18,15z"/> </svg>`; const onOffButtons = [{ text: 'On' }, { text: 'Off' }]; const buttonsStyle = { containerHeight: 60, height: 40, width: 82, marginRight: 15, marginTop: 10 }; const parameterItemMeta: ICustomItemMetaData = { customProperties: [{ ownerType: Model.CustomItem, propertyName: 'showHeaders', valueType: 'string', defaultValue: 'On', },{ ownerType: Model.CustomItem, propertyName: 'showParameterName', valueType: 'string', defaultValue: 'On', },{ ownerType: Model.CustomItem, propertyName: 'automaticUpdates', valueType: 'string', defaultValue: 'Off', }], optionsPanelSections: [{ title: 'Parameters settings', items: [{ dataField: 'showHeaders', template: FormItemTemplates.buttonGroup, editorOptions: { items: onOffButtons, }, }, { dataField: 'showParameterName', template: FormItemTemplates.buttonGroup, editorOptions: { items: onOffButtons, }, }, { dataField: 'automaticUpdates', template: FormItemTemplates.buttonGroup, editorOptions: { items: onOffButtons, }, }], }], icon: PARAMETER_EXTENSION_NAME, title: "Parameters" }; export class ParameterItemExtension implements ICustomItemExtension { name = PARAMETER_EXTENSION_NAME; metaData = parameterItemMeta; constructor(private dashboardControl: any) { dashboardControl.registerIcon(svgIcon); } public createViewerItem = (model: any, element: any, content: any) => { var parametersExtension = this.dashboardControl.findExtension("dashboard-parameter-dialog"); if (!parametersExtension){ throw Error('The "dashboard-parameter-dialog" extension does not exist. To register this extension, use the DashboardControl.registerExtension method.'); } return new ParameterItem(model, element, content, parametersExtension); } } export class ParameterItem extends CustomItemViewer { gridContainer?: HTMLElement; buttonContainer?: HTMLElement; parametersExtension: any; parametersContent: any; dialogButtonSubscribe: any; buttons: dxButton[] = []; _element: HTMLElement; constructor(model: any, container: any, options: any, parametersExtension: any) { super(model, container, options); this.parametersExtension = parametersExtension; this._subscribeProperties(); this.parametersExtension.showDialogButton(false); this.parametersExtension.subscribeToContentChanges(() => { this._generateParametersContent(); }); this.dialogButtonSubscribe = this.parametersExtension.showDialogButton.subscribe(() => { this.parametersExtension.showDialogButton(false); }); } override setSize(width: number, height: number): void { super.setSize(width, height); this._setGridHeight(); } override dispose(): void { super.dispose(); this.parametersContent && this.parametersContent.dispose && this.parametersContent.dispose(); this.dialogButtonSubscribe.dispose(); this.parametersExtension.showDialogButton(true); this.buttons.forEach(button => button.dispose()); } override renderContent(element: HTMLElement, changeExisting: boolean, afterRenderCallback?: any) { this._element = element; if (!changeExisting) { element.innerHTML = ''; this.buttons.forEach(button => button.dispose()); element.style.overflow = 'auto'; this.gridContainer = document.createElement('div'); element.appendChild(this.gridContainer); this._generateParametersContent(); this.buttonContainer = document.createElement('div'); this.buttonContainer.style.height = buttonsStyle.containerHeight + 'px', this.buttonContainer.style.width = buttonsStyle.width * 2 + buttonsStyle.marginRight * 2 + 'px', this.buttonContainer.style.cssFloat = 'right' element.appendChild(this.buttonContainer); this.buttons.push(this._createButton(this.buttonContainer, "Reset", () => { this.parametersContent.resetParameterValues(); })); this.buttons.push(this._createButton(this.buttonContainer, "Submit", () => { this._submitValues(); })); if (this.getPropertyValue('automaticUpdates') != 'Off') this.buttonContainer.style.display = 'none'; } } _generateParametersContent() { this.parametersContent = this.parametersExtension.renderContent(this.gridContainer); this.parametersContent.valueChanged.add(() => this._updateParameterValues()); this._setGridHeight(); this._update({ showHeaders: this.getPropertyValue('showHeaders'), showParameterName: this.getPropertyValue('showParameterName') }); } _submitValues() { this.parametersContent.submitParameterValues(); this._update({ showHeaders: this.getPropertyValue('showHeaders'), showParameterName: this.getPropertyValue('showParameterName') }); } _updateParameterValues() { this.getPropertyValue('automaticUpdates') != 'Off' ? this._submitValues() : null; } _setGridHeight() { var gridHeight = this.contentHeight(); if (this.getPropertyValue('automaticUpdates') === 'Off') gridHeight -= buttonsStyle.containerHeight; this.parametersContent.grid.option('height', gridHeight); } _createButton(container: HTMLElement, buttonText: string, onClick: () => void): dxButton { let button = document.createElement("div"); button.style.marginRight = buttonsStyle.marginRight + 'px'; button.style.marginTop = buttonsStyle.marginTop + 'px'; container.appendChild(button); return new dxButton(button, { text: buttonText, height: buttonsStyle.height + 'px', width: buttonsStyle.width + 'px', onClick: onClick }); } _subscribeProperties() { this.subscribe('showHeaders', (showHeaders) => { this._update({ showHeaders: showHeaders }); }); this.subscribe('showParameterName', (showParameterName) => { this._update({ showParameterName: showParameterName, rerender: true }); }); this.subscribe('automaticUpdates', (automaticUpdates) => { this._update({ automaticUpdates: automaticUpdates }) }); }; _update(options: any) { if (!!options.showHeaders) { this.parametersContent.grid.option('showColumnHeaders', options.showHeaders === 'On'); } if(!!options.showParameterName) { this.parametersContent.valueChanged.empty(); this.parametersContent.grid.columnOption(0, 'visible', options.showParameterName === 'On'); this.parametersContent.valueChanged.add(() => { return this._updateParameterValues(); }); } if(!!options.automaticUpdates) { if (this.buttonContainer){ if (options.automaticUpdates == 'Off') { this.buttonContainer.style.display = 'block'; } else { this.buttonContainer.style.display = 'none'; } } } this._setGridHeight(); if(options.rerender) this.renderContent(this._element, false); } }
dashboard-angular-app/src/app/extensions/online-map-item.ts
TypeScript
import * as Model from 'devexpress-dashboard/model'; import { FormItemTemplates } from 'devexpress-dashboard/designer'; import { ICustomItemExtension, CustomItemViewer } from 'devexpress-dashboard/common'; import { ICustomItemMetaData } from 'devexpress-dashboard/model/items/custom-item/meta'; import dxMap from 'devextreme/ui/map' const ONLINE_MAP_EXTENSION_NAME = 'OnlineMap'; const svgIcon = `<svg version="1.1" id="` + ONLINE_MAP_EXTENSION_NAME + `" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <path class="dx_darkgray" d="M12,1C8.1,1,5,4.1,5,8c0,3.9,3,10,7,15c4-5,7-11.1,7-15C19,4.1,15.9,1,12,1z M12,12c-2.2,0-4-1.8-4-4 c0-2.2,1.8-4,4-4s4,1.8,4,4C16,10.2,14.2,12,12,12z"/> <circle class="dx_red" cx="12" cy="8" r="2"/> </svg>`; const onlineMapMeta: ICustomItemMetaData = { bindings:[{ propertyName: 'Latitude', dataItemType: 'Dimension', array: false, enableInteractivity: true, displayName: 'Latitude', emptyPlaceholder: 'Set Latitude', selectedPlaceholder: 'Configure Latitude', constraints: { allowedTypes: ['Integer', 'Float', 'Double', 'Decimal'] } }, { propertyName: 'Longitude', dataItemType: 'Dimension', array: false, enableInteractivity: true, displayName: 'Longitude', emptyPlaceholder: 'Set Longitude', selectedPlaceholder: 'Configure Longitude', constraints: { allowedTypes: ['Integer', 'Float', 'Double', 'Decimal'] } }], customProperties: [{ ownerType: Model.CustomItem, propertyName: 'Provider', valueType: 'string', defaultValue: 'Bing', },{ ownerType: Model.CustomItem, propertyName: 'Type', valueType: 'string', defaultValue: 'RoadMap', },{ ownerType: Model.CustomItem, propertyName: 'DisplayMode', valueType: 'string', defaultValue: 'Markers', }], optionsPanelSections: [{ title: 'Custom Options', items: [{ dataField: 'Provider', template: FormItemTemplates.buttonGroup, editorOptions: { items: [{ text: 'Google' }, { text: 'Bing' }] }, },{ dataField: 'Type', template: FormItemTemplates.buttonGroup, editorOptions: { items: [{ text: 'RoadMap' }, { text: 'Satellite' }, { text: 'Hybrid' }] }, },{ dataField: 'DisplayMode', template: FormItemTemplates.buttonGroup, editorOptions: { keyExpr: 'value', items: [{ value: 'Markers', text: 'Markers' }, { value: 'Routes', text: 'Routes' }, { value: 'MarkersAndRoutes', text: 'All' }], }, }] }], interactivity: { filter: true, drillDown: false }, icon: ONLINE_MAP_EXTENSION_NAME, title: 'Online Map' }; export class OnlineMapItemExtension implements ICustomItemExtension { name = ONLINE_MAP_EXTENSION_NAME; metaData = onlineMapMeta; constructor(dashboardControl: any) { dashboardControl.registerIcon(svgIcon); } public createViewerItem = (model: any, element: any, content: any) => { return new OnlineMapItem(model, element, content); } } export class OnlineMapItem extends CustomItemViewer { private mapViewer?: any; constructor(model: any, container:any, options:any) { super(model, container, options); } override setSize(width: number, height: number): void { super.setSize(width, height); let contentWidth = this.contentWidth(), contentHeight = this.contentHeight(); this.mapViewer.option('width', contentWidth); this.mapViewer.option('height', contentHeight); } override setSelection(values: Array<Array<any>>): void { super.setSelection(values); this._updateSelection(); } override clearSelection(): void { super.clearSelection(); this._updateSelection(); } override renderContent(element: HTMLElement, changeExisting: boolean, afterRenderCallback?: any) { let markers: any[] = [], routes: any[] = [], mode = this.getPropertyValue('DisplayMode'), showMarkers = mode === 'Markers' || mode === 'MarkersAndRoutes' || this.canMasterFilter(), showRoutes = mode === 'Routes' || mode === 'MarkersAndRoutes'; if(this.getBindingValue('Latitude').length > 0 && this.getBindingValue('Longitude').length > 0) { this.iterateData(row => { let latitude = row.getValue('Latitude')[0]; let longitude = row.getValue('Longitude')[0]; if (latitude && longitude) { if (showMarkers) { markers.push({ location: { lat: latitude, lng: longitude }, iconSrc: this.isSelected(row) ? "https://js.devexpress.com/Demos/WidgetsGallery/JSDemos/images/maps/map-marker.png" : null, onClick: (args: any) => { this._onClick(row); }, tag: row }); } if (showRoutes) { routes.push([latitude, longitude]); } } }); } let autoAdjust = markers.length > 1 || routes.length > 1, options = <any>{ provider: (<string>this.getPropertyValue('Provider')).toLowerCase(), type: (<string>this.getPropertyValue('Type')).toLowerCase(), controls: true, zoom: autoAdjust ? 1000 : 1, autoAdjust: autoAdjust, width: this.contentWidth(), height: this.contentHeight(), // Use the template below to authenticate the application within the required map provider. //apiKey: { // bing: 'BINGAPIKEY', // google: 'GOOGLEAPIKEY' //}, markers: markers, routes: routes.length > 0 ? [{ weight: 6, color: 'blue', opacity: 0.5, mode: '', locations: routes }] : [] }; if(changeExisting && this.mapViewer) { this.mapViewer.option(options); } else { this.mapViewer = new dxMap(element, options); } } private _onClick(row: any) { this.setMasterFilter(row); this._updateSelection(); } private _updateSelection() { let markers = this.mapViewer.option('markers'); markers.forEach((marker: any) => { marker.iconSrc = this.isSelected(marker.tag) ? "https://js.devexpress.com/Demos/WidgetsGallery/JSDemos/images/maps/map-marker.png" : null; }); this.mapViewer.option('autoAdjust', false); this.mapViewer.option('markers', markers); } }
dashboard-angular-app/src/app/extensions/webpage-item.ts
TypeScript
import * as Model from 'devexpress-dashboard/model'; import { ICustomItemExtension, CustomItemViewer } from 'devexpress-dashboard/common'; import { ICustomItemMetaData } from 'devexpress-dashboard/model/items/custom-item/meta'; const WEBPAGE_EXTENSION_NAME = 'WebPage'; const svgIcon = `<?xml version="1.0" encoding="utf-8"?> <svg version = "1.1" id = "` + WEBPAGE_EXTENSION_NAME + `" xmlns = "http://www.w3.org/2000/svg" xmlns: xlink = "http://www.w3.org/1999/xlink" x = "0px" y = "0px" viewBox = "0 0 24 24" style = "enable-background:new 0 0 24 24;" xml: space = "preserve" > <path class="dx-dashboard-contrast-icon" d="M20.7,4.7l-3.4-3.4C17.1,1.1,16.9,1,16.6,1H4C3.4,1,3,1.4,3,2v20c0,0.6,0.4,1,1,1h16 c0.6,0,1-0.4,1-1V5.4C21,5.1,20.9,4.9,20.7,4.7z M19,21H5V3h11v2c0,0.6,0.4,1,1,1h2V21z"/> <path class="dx-dashboard-accent-icon" d="M13.7,17.5c-0.2-0.4-1.6-1.8-1.4-2.2s0.2-1.1-0.1-1.3c-0.3-0.1-0.7,0.1-0.7-0.2 c-0.1-0.3-1.1-0.2-1.2-1.6c-0.1-1.5-0.6-2-1.2-2s-1.6,0.6-1.5,0c0-0.1,0-0.2,0-0.3c-1,1-1.6,2.5-1.6,4.1c0,3.3,2.7,6,6,6 c0.6,0,1.1-0.1,1.6-0.2C13.7,19.1,13.9,17.8,13.7,17.5z M12,8c-1.1,0-2.2,0.3-3.1,0.9H9c1,0.2,3.1,0.7,3.1,0.3S12,8.3,12.2,8.4 c0.2,0.2,0.8,0.7,0.6,1S12,10,12.2,10.3c0.2,0.2,0.8,0.6,1,0.4s-0.1-0.9,0.2-0.8c0.3,0,1.8,0.8,1.3,1.1s-1.4,1.9-1.9,2 s-0.9,0.2-0.8,0.6c0.2,0.5,0.5,0.2,0.7,0.3c0.1,0.1,0.1,0.4,0.3,0.6s0.4,0.1,0.7,0.1c0.3-0.1,2.5,0.9,2.3,1.4 c-0.2,0.5-0.2,1.2-1,2.1c-0.5,0.5-0.7,1.1-0.9,1.5c2.3-0.8,4-3,4-5.6C18,10.7,15.3,8,12,8z"/> </svg>`; const webPageMeta: ICustomItemMetaData = { bindings: [{ propertyName: 'Attribute', dataItemType: 'Dimension', array: false, displayName: "Attribute", emptyPlaceholder: 'Set Attribute', selectedPlaceholder: "Configure Attribute" }], customProperties: [{ ownerType: Model.CustomItem, propertyName: 'Url', valueType: 'string', defaultValue: 'https://en.wikipedia.org/wiki/{0}', }], optionsPanelSections: [{ title: 'Custom Options', items: [{ dataField: 'Url', editorType: 'dxTextBox', }] }], icon: WEBPAGE_EXTENSION_NAME, title: "Web Page" }; export class WebPageItemExtension implements ICustomItemExtension { name = WEBPAGE_EXTENSION_NAME; metaData = webPageMeta; constructor(dashboardControl: any) { dashboardControl.registerIcon(svgIcon); } public createViewerItem = (model: any, element: any, content: any) => { return new WebPageItem(model, element, content); } } export class WebPageItem extends CustomItemViewer { private iframe?: HTMLIFrameElement; constructor(model: any, container: any, options: any) { super(model, container, options); } override renderContent(element: HTMLElement, changeExisting: boolean, afterRenderCallback?: any) { let attribute: string = ""; if(!changeExisting || !this.iframe) { while (element.firstChild) element.removeChild(element.firstChild); this.iframe = <HTMLIFrameElement>(document.createElement('iframe')); this.iframe.setAttribute('width', '100%'); this.iframe.setAttribute('height', '100%'); this.iframe.setAttribute('style', 'border: none;'); element.append(this.iframe); } this.iterateData(row => { if(!attribute) attribute = row.getDisplayText('Attribute')[0]; }); this.iframe.setAttribute('src', (<string>this.getPropertyValue('Url')).replace('{0}', attribute)); } }
dashboard-angular-app/src/app/extensions/gantt-item.ts
TypeScript
import { ICustomItemExtension, CustomItemViewer } from 'devexpress-dashboard/common'; import { ICustomItemMetaData } from 'devexpress-dashboard/model/items/custom-item/meta'; import dxGantt from 'devextreme/ui/gantt'; import notify from "devextreme/ui/notify"; const GANTT_EXTENSION_NAME = 'GanttItem'; const svgIcon = `<?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="` + GANTT_EXTENSION_NAME + `" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <path class="dx-dashboard-contrast-icon" d="M23,2c0-0.6-0.4-1-1-1H2C1.4,1,1,1.4,1,2v20c0,0.6,0.4,1,1,1h20c0.6,0,1-0.4,1-1 V2z M21,21H3V3h18V21z"/> <path class="dx-dashboard-accent-icon" d="M12,9H5V5h7V9z M19,10H9v4h10V10z M15,15H7v4h8V15z"/> </svg>`; const ganttItemMeta: ICustomItemMetaData = { bindings: [{ propertyName: 'ID', dataItemType: 'Dimension', displayName: 'ID', array: false, enableInteractivity: true, emptyPlaceholder: 'Set ID', selectedPlaceholder: 'Configure ID' }, { propertyName: 'ParentID', dataItemType: 'Dimension', displayName: 'Parent ID', array: false, enableInteractivity: true, emptyPlaceholder: 'Set Parent ID', selectedPlaceholder: 'Configure Parent ID' }, { propertyName: 'Text', dataItemType: 'Dimension', displayName: 'Text', array: false, enableInteractivity: true, emptyPlaceholder: 'Set Text', selectedPlaceholder: 'Configure Text' }, { propertyName: 'StartDate', dataItemType: 'Dimension', displayName: 'Start Date', array: false, enableInteractivity: true, emptyPlaceholder: 'Set Start Date', selectedPlaceholder: 'Configure Start Date' }, { propertyName: 'FinishDate', dataItemType: 'Dimension', displayName: 'Finish Date', array: false, enableInteractivity: true, emptyPlaceholder: 'Set Finish Date', selectedPlaceholder: 'Configure Finish Date' }], interactivity: { filter: true }, icon: GANTT_EXTENSION_NAME, title: 'Gantt Chart' }; export class GanttItemExtension implements ICustomItemExtension { name = GANTT_EXTENSION_NAME; metaData = ganttItemMeta; constructor(dashboardControl: any) { dashboardControl.registerIcon(svgIcon); } public createViewerItem = (model: any, element: any, content: any) => { return new GanttItemViewer(model, element, content); } } export class GanttItemViewer extends CustomItemViewer { dxGanttWidget?: dxGantt; constructor(model: any, container: any, options: any) { super(model, container, options); } _getDataSource() { let data: any[] = []; let datesValid: boolean = true; this.iterateData(function (dataRow) { data.push({ id: dataRow.getValue('ID')[0], parentId: dataRow.getValue('ParentID')[0], title: dataRow.getValue('Text')[0], start: dataRow.getValue('StartDate')[0], end: dataRow.getValue('FinishDate')[0], clientDataRow: dataRow }); let currentItem = data[data.length - 1]; if ((currentItem.start && !(currentItem.start instanceof Date)) || (currentItem.end && !(currentItem.end instanceof Date))) datesValid = false; }); if (!datesValid) { notify("Gantt: 'Start Date' or 'Finish Date' is not a Date object.", "warning", 3000); return []; } return data; }; _getDxGanttWidgetSettings() { return { rootValue: -1, tasks: { dataSource: this._getDataSource() }, columns: [{ dataField: "title", caption: "Subject", width: 300, }, { dataField: "start", caption: "Start Date" }, { dataField: "end", caption: "End Date" }], onTaskClick: (e: any) => { let tasks = e.component.option("tasks.dataSource"); let clickedTask = tasks.filter((item: any) => item.id === e.key)[0]; this.setMasterFilter(clickedTask.clientDataRow); }, scaleType: "days", taskListWidth: 500 }; } override renderContent(element: HTMLElement, changeExisting: boolean, afterRenderCallback?: any) { if (!changeExisting) { while (element.firstChild) element.removeChild(element.firstChild); this.dxGanttWidget = new dxGantt(element, <any>this._getDxGanttWidgetSettings()); } else { this.dxGanttWidget?.option(<any>this._getDxGanttWidgetSettings()); } } override setSelection(values: Array<Array<any>>): void { super.setSelection(values); let tasks: any = this.dxGanttWidget?.option("tasks.dataSource"); tasks.forEach((item: any) => { if (this.isSelected(item.clientDataRow)) this.dxGanttWidget?.option("selectedRowKey", item.id); }); } override clearSelection(): void { super.clearSelection(); this.dxGanttWidget?.option("selectedRowKey", null); } override setSize(width: number, height: number): void { super.setSize(width, height); this.dxGanttWidget?.repaint(); } }
dashboard-angular-app/src/app/extensions/hierarchical-tree-view-item.ts
TypeScript
import { ICustomItemExtension, CustomItemViewer, DashboardControl } from 'devexpress-dashboard/common'; import { ICustomItemMetaData } from 'devexpress-dashboard/model/items/custom-item/meta'; import dxTreeView, { Node, Properties } from 'devextreme/ui/tree_view'; const HIERARCIAL_TREE_VIEW_EXTENSION_NAME = 'TreeView'; const svgIcon = `<?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="` + HIERARCIAL_TREE_VIEW_EXTENSION_NAME + `" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <polygon class="dx-dashboard-contrast-icon" points="12,13 12,11 8,11 8,8 6,8 6,21 12,21 12,19 8,19 8,13 "/> <path class="dx-dashboard-accent-icon" d="M10,7H4C3.5,7,3,6.6,3,6V2c0-0.5,0.5-1,1-1h6c0.6,0,1,0.5,1,1v4C11,6.6,10.6,7,10,7z M21,14v-4c0-0.6-0.5-1-1-1h-6c-0.6,0-1,0.4-1,1v4c0,0.6,0.4,1,1,1h6C20.5,15,21,14.6,21,14z M21,22v-4c0-0.5-0.5-1-1-1h-6 c-0.6,0-1,0.5-1,1v4c0,0.5,0.4,1,1,1h6C20.5,23,21,22.5,21,22z"/> </svg>`; const treeViewMeta: ICustomItemMetaData = { bindings: [{ propertyName: 'idBinding', dataItemType: 'Dimension', array: false, displayName: 'ID', emptyPlaceholder: 'Add ID', selectedPlaceholder: 'Configure ID', }, { propertyName: 'parentIdBinding', dataItemType: 'Dimension', array: false, displayName: 'Parent ID', emptyPlaceholder: 'Add Parent ID', selectedPlaceholder: 'Configure Parent ID', }, { propertyName: 'dimensionsBinding', dataItemType: 'Dimension', array: false, displayName: 'Dimensions', emptyPlaceholder: 'Add Dimension', selectedPlaceholder: 'Configure Dimension', enableInteractivity: true }], interactivity: { filter: true }, icon: HIERARCIAL_TREE_VIEW_EXTENSION_NAME, // Uncomment the line below to place this custom item in the "Filter" group: //groupName: 'filter', title: 'Hierarchical Tree View' }; export class TreeViewItemExtension implements ICustomItemExtension { name = HIERARCIAL_TREE_VIEW_EXTENSION_NAME; metaData = <ICustomItemMetaData>treeViewMeta; dashboardControl: DashboardControl; constructor(dashboardControl: DashboardControl) { this.dashboardControl = dashboardControl; dashboardControl.registerIcon(svgIcon); } public createViewerItem = (model: any, element: any, content: any) => { return new TreeViewItem(model, element, content, this.dashboardControl); } } export class TreeViewItem extends CustomItemViewer { private dxTreeViewWidget?: dxTreeView; private _requiredBindingsCount: number; private dashboardControl: DashboardControl; constructor(model: any, container: any, options: any, dashboardControl: DashboardControl) { super(model, container, options); this._requiredBindingsCount = 3; this.dashboardControl = dashboardControl; } override renderContent(element: HTMLElement, changeExisting: boolean, afterRenderCallback?: any) { let dataSource: any[] = []; //Check Bindings let bindings = this.getBindingValue('dimensionsBinding').concat(this.getBindingValue('idBinding')).concat(this.getBindingValue('parentIdBinding')); if (bindings.length !== this._requiredBindingsCount) return; //Get Data Source this.iterateData(function (dataRow) { let row = <any>{ ID: dataRow.getDisplayText('idBinding')[0], ParentID: dataRow.getDisplayText('parentIdBinding')[0] !== '-1' ? dataRow.getDisplayText('parentIdBinding')[0] : null, DisplayField: dataRow.getDisplayText('dimensionsBinding')[0], }; row._customData = dataRow; dataSource.push(row); }); let treeViewOptions: Properties = { items: dataSource, dataStructure: "plain", parentIdExpr: "ParentID", keyExpr: "ID", displayExpr: "DisplayField", selectionMode: "multiple", selectNodesRecursive: false, onItemClick: e => { if(this.getMasterFilterMode() === 'Multiple' && this.allowMultiselection) { this.setMasterFilterRecursive(<any>e.node); } else { this.setMasterFilter((<any>e.itemData)._customData); } }, onContentReady: e => { this.updateTreeViewSelection(); } }; if (!changeExisting) { while (element.firstChild) element.removeChild(element.firstChild); let div = document.createElement('div'); element.appendChild(div); this.dxTreeViewWidget = new dxTreeView(div, treeViewOptions); } else { this.dxTreeViewWidget?.option(treeViewOptions); } } override clearSelection(): void { super.clearSelection(); this.updateTreeViewSelection(); } override setSelection(values: Array<Array<any>>): void { super.setSelection(values); this.updateTreeViewSelection(); } updateTreeViewSelection() { if (this.dxTreeViewWidget) { this.dxTreeViewWidget.unselectAll(); let nodes: any = this.dxTreeViewWidget.option('items'); nodes.forEach((item: any) => { if(this.isSelected(item._customData)) this.dxTreeViewWidget?.selectItem(item.ID); }); } } setMasterFilterRecursive(node: Node) { this.setMasterFilter((<any>node.itemData)._customData); node.children?.forEach(x => this.setMasterFilterRecursive(x)); } }
dashboard-angular-app/src/app/extensions/funnel-d3-item.ts
TypeScript
import { ICustomItemExtension, CustomItemViewer } from 'devexpress-dashboard/common'; import { FormItemTemplates } from 'devexpress-dashboard/designer'; import * as Model from 'devexpress-dashboard/model'; import { ICustomItemMetaData } from 'devexpress-dashboard/model/items/custom-item/meta'; import D3Funnel from 'd3-funnel'; import $ from 'jquery'; const FUNNEL_D3_EXTENSION_NAME = 'FunnelD3'; const svgIcon = '<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 21.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) --><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" id="' + FUNNEL_D3_EXTENSION_NAME + '" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"><polygon class="dx_green" points="2,1 22,1 16,8 8,8 "/><polygon class="dx_blue" points="8,9 16,9 14,15 10,15 "/><polygon class="dx_red" points="10,16 14,16 13,23 11,23 "/></svg>'; const funnelMeta: ICustomItemMetaData = { bindings: [{ propertyName: 'Values', dataItemType: 'Measure', array: true, enableColoring: true, displayName: 'Values', emptyPlaceholder: 'Set Value', selectedPlaceholder: 'Configure Value' }, { propertyName: 'Arguments', dataItemType: 'Dimension', array: true, enableInteractivity: true, enableColoring: true, displayName: 'Arguments', emptyPlaceholder: 'Set Argument', selectedPlaceholder: 'Configure Argument' }], customProperties: [{ ownerType: Model.CustomItem, propertyName: 'FillType', valueType: 'string', defaultValue: 'Solid', },{ ownerType: Model.CustomItem, propertyName: 'IsCurved', valueType: 'boolean', defaultValue: false, },{ ownerType: Model.CustomItem, propertyName: 'IsDynamicHeight', valueType: 'boolean', defaultValue: true, }, { ownerType: Model.CustomItem, propertyName: 'PinchCount', valueType: 'number', defaultValue: 0, }], optionsPanelSections: [{ title: 'Settings', items: [{ dataField: 'FillType', template: FormItemTemplates.buttonGroup, editorOptions: { items: [{ text: 'Solid' }, { text: 'Gradient' }] }, },{ dataField: 'IsCurved', label: { text: 'Curved' }, template: FormItemTemplates.buttonGroup, editorOptions: { keyExpr: 'value', items: [{ value: false, text: 'No', }, { value: true, text: 'Yes', }] }, },{ dataField: 'IsDynamicHeight', label: { text: 'Dynamic Height' }, template: FormItemTemplates.buttonGroup, editorOptions: { keyExpr: 'value', items: [{ value: false, text: 'No', }, { value: true, text: 'Yes', }] }, }, { dataField: 'PinchCount', editorType: 'dxNumberBox', editorOptions: { min: 0, }, }], }], interactivity: { filter: true, drillDown: true }, icon: FUNNEL_D3_EXTENSION_NAME, title: 'Funnel D3', index: 3 }; class FunnelD3ItemViewer extends CustomItemViewer { funnelSettings; funnelViewer; selectionValues: Array<any> exportingImage: HTMLImageElement; funnelContainer: HTMLElement; constructor(model, container, options) { super(model, container, options); this.funnelSettings = undefined; this.funnelViewer = null; this.selectionValues = []; this.exportingImage = new Image(); this._subscribeProperties(); } override renderContent(element, changeExisting) { let htmlElement: HTMLElement = element instanceof $ ? (<JQuery>element).get(0): <HTMLElement>(<any>element); var data = this._getDataSource(); if(!this._ensureFunnelLibrary(htmlElement)) return; if(!!data) { if(!changeExisting || !this.funnelViewer) { while(htmlElement.firstChild) htmlElement.removeChild(htmlElement.firstChild); this.funnelContainer = document.createElement('div'); this.funnelContainer.style.margin = '20px'; this.funnelContainer.style.height = 'calc(100% - 40px)' htmlElement.appendChild(this.funnelContainer); this.funnelViewer = new D3Funnel(this.funnelContainer); } this._update(data, this._getFunnelSizeOptions()); } else { while(htmlElement.firstChild) htmlElement.removeChild(htmlElement.firstChild); this.funnelViewer = null; } }; override setSize (width, height) { super.setSize(width, height); this._update(null, this._getFunnelSizeOptions()); }; override setSelection(values: Array<Array<any>>) { super.setSelection(values); this._update(this._getDataSource()); }; override clearSelection() { super.clearSelection(); this._update(this._getDataSource()); }; override allowExportSingleItem() { return !this._isIEBrowser(); }; override getExportInfo () { if (this._isIEBrowser()) return void 0; return { image: this._getImageBase64() }; }; _getFunnelSizeOptions () { if(!this.funnelContainer) return { }; return { chart: { width: this.funnelContainer.clientWidth, height:this.funnelContainer.clientHeight } }; }; _getDataSource() { var bindingValues = this.getBindingValue('Values'); if(bindingValues.length == 0) return undefined; var data = []; this.iterateData((dataRow) => { var values = dataRow.getValue('Values'); var valueStr = dataRow.getDisplayText('Values'); var color = dataRow.getColor('Values'); if(this._hasArguments()) { var labelText = dataRow.getDisplayText('Arguments').join(' - ') + ': ' + valueStr; data.push([{ data: dataRow, text: labelText, color: color[0] }].concat(values));//0 - 'layer' index for color value } else { data = values.map((value, index) => { return [{ text: bindingValues[index].displayName() + ': ' + valueStr[index], color: color[index] }, value]; }); } }); return data.length > 0 ? data : undefined; }; _ensureFunnelLibrary(htmlElement: HTMLElement) { if(!D3Funnel) { htmlElement.innerHTML = ''; var textDiv = document.createElement('div'); textDiv.style.position= 'absolute'; textDiv.style.top= '50%'; textDiv.style.transform= 'translateY(-50%)'; textDiv.style.width= '95%'; textDiv.style.color= '#CF0F2E'; textDiv.style.textAlign= 'center'; textDiv.innerText = "'D3Funnel' cannot be displayed. You should include 'd3.v3.min.js' and 'd3-funnel.js' libraries." htmlElement.appendChild(textDiv); return false; } return true; }; _ensureFunnelSettings() { var getSelectionColor = (hexColor) => { return this.funnelViewer.colorizer.shade(hexColor, -0.5); }; if(!this.funnelSettings) { this.funnelSettings = { data: undefined, options: { chart: { bottomPinch: this.getPropertyValue('PinchCount'), curve: { enabled: this.getPropertyValue('IsCurved') } }, block: { dynamicHeight: this.getPropertyValue('IsDynamicHeight'), fill: { scale: (index) => { var obj = this.funnelSettings.data[index][0]; return obj.data && this.isSelected(obj.data) ? getSelectionColor(obj.color) : obj.color; }, type: (<string>this.getPropertyValue('FillType')).toLowerCase() } }, label: { format: (label, value) => { return label.text; } }, events: { click: { block: (e) => this._onClick(e) } } } }; } this.funnelSettings.options.block.highlight = this.canDrillDown() || this.canMasterFilter(); return this.funnelSettings; }; _onClick(e) { if(!this._hasArguments() || !e.label) return; var row = e.label.raw.data; if (this.canDrillDown(row)) this.drillDown(row); else if (this.canMasterFilter(row)) { this.setMasterFilter(row); this._update(); } }; _subscribeProperties() { this.subscribe('IsCurved', (isCurved) => this._update(null, { chart: { curve: { enabled: isCurved } } }) ); this.subscribe('IsDynamicHeight', (isDynamicHeight) => this._update(null, { block: { dynamicHeight: isDynamicHeight } })); this.subscribe('PinchCount', (count) => this._update(null, { chart: { bottomPinch: count } })); this.subscribe('FillType', (type)=> this._update(null, { block: { fill: { type: type.toLowerCase() } } })); }; _update(data?, options?) { this._ensureFunnelSettings(); if(!!data) { this.funnelSettings.data = data; } if(!!options) { $.extend(true, this.funnelSettings.options, options); } if(!!this.funnelViewer) { this.funnelViewer.draw(this.funnelSettings.data, this.funnelSettings.options); this._updateExportingImage(); } }; _updateExportingImage () { var svg = this.funnelContainer.firstElementChild, str = new XMLSerializer().serializeToString(svg), encodedData = 'data:image/svg+xml;base64,' + window.btoa(window["unescape"](encodeURIComponent(str))); this.exportingImage.src = encodedData; }; _hasArguments() { return this.getBindingValue('Arguments').length > 0; }; _getImageBase64 () { var canvas = document.createElement('canvas');; canvas.width = this.funnelContainer.clientWidth; canvas.height = this.funnelContainer.clientHeight; canvas.getContext('2d').drawImage(this.exportingImage, 0, 0); return canvas.toDataURL().replace('data:image/png;base64,', ''); } _isIEBrowser () { return navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0; } } export class FunnelD3ItemExtension implements ICustomItemExtension { name = FUNNEL_D3_EXTENSION_NAME; metaData = funnelMeta; constructor(dashboardControl: any) { dashboardControl.registerIcon(svgIcon); } public createViewerItem = (model: any, element: any, content: any) => { return new FunnelD3ItemViewer(model, element, content); } }
dashboard-angular-app/src/app/app.component.ts
TypeScript
import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterOutlet } from '@angular/router'; import { DxDashboardControlModule } from 'devexpress-dashboard-angular'; import { DevExtremeModule } from 'devextreme-angular'; import { DashboardControlArgs, DashboardPanelExtension } from 'devexpress-dashboard'; import { FunnelD3ItemExtension } from './extensions/funnel-d3-item'; import { GanttItemExtension } from './extensions/gantt-item'; import { TreeViewItemExtension } from './extensions/hierarchical-tree-view-item'; import { OnlineMapItemExtension } from './extensions/online-map-item'; import { ParameterItemExtension } from './extensions/parameter-item'; import { PolarChartItemExtension } from './extensions/polar-chart-item'; import { SimpleTableItemExtension } from './extensions/simple-table-item'; import { WebPageItemExtension } from './extensions/webpage-item'; @Component({ selector: 'app-root', standalone: true, imports: [CommonModule, RouterOutlet, DxDashboardControlModule, DevExtremeModule], templateUrl: './app.component.html', styleUrl: './app.component.css' }) export class AppComponent { constructor() { } onBeforeRender(e: DashboardControlArgs) { let dashboardControl = e.component; dashboardControl.registerExtension(new DashboardPanelExtension(dashboardControl)); dashboardControl.registerExtension(new WebPageItemExtension(dashboardControl)); dashboardControl.registerExtension(new SimpleTableItemExtension(dashboardControl)); dashboardControl.registerExtension(new OnlineMapItemExtension(dashboardControl)); dashboardControl.registerExtension(new PolarChartItemExtension(dashboardControl)); dashboardControl.registerExtension(new GanttItemExtension(dashboardControl)); dashboardControl.registerExtension(new TreeViewItemExtension(dashboardControl)); dashboardControl.registerExtension(new ParameterItemExtension(dashboardControl)); dashboardControl.registerExtension(new FunnelD3ItemExtension(dashboardControl)); } }
dashboard-angular-app/src/app/app.component.html
HTML
<dx-dashboard-control style="display: block;width:100%;height:800px;" endpoint="http://localhost:5000/api/dashboard" (onBeforeRender)="onBeforeRender($event)"> </dx-dashboard-control>

Disclaimer: The information provided on DevExpress.com and affiliated web properties (including the DevExpress Support Center) is provided "as is" without warranty of any kind. Developer Express Inc disclaims all warranties, either express or implied, including the warranties of merchantability and fitness for a particular purpose. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.

Confidential Information: Developer Express Inc does not wish to receive, will not act to procure, nor will it solicit, confidential or proprietary materials and information from you through the DevExpress Support Center or its web properties. Any and all materials or information divulged during chats, email communications, online discussions, Support Center tickets, or made available to Developer Express Inc in any manner will be deemed NOT to be confidential by Developer Express Inc. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.