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:
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.
- The asp-net-core-server folder contains the backend project built with ASP.NET Core 6.0.
- The dashboard-angular-app folder contains the client application built with React.
Files to Review
- simple-table-item.ts
- polar-chart-item.ts
- parameter-item.ts
- online-map-item.ts
- webpage-item.ts
- gantt-item.ts
- hierarchical-tree-view-item.ts
- funnel-d3-item.ts
- app.component.ts
- app.component.html
Quick Start
Server
Run the following command in the asp-net-core-server folder:
Codedotnet 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:
Codenpm 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:
- 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:
- 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:
- 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:
- 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:
- Provider - Specifies whether to show Google or Bing maps.
- Type - Specifies the map type. You can choose between
RoadMap
,Satellite
orHybrid
. - 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:
- 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.
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.
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
- Dashboard for ASP.NET Core - Custom Item Gallery
- Dashboard for ASP.NET Core - Custom Item Tutorials
- Dashboard for React - Custom Item Tutorials
- Dashboard for React - Custom Item Gallery
- Dashboard for WinForms - Custom Item Extensions
Does this example address your development requirements/objectives?
(you will be redirected to DevExpress.com to submit your response)
Example Code
TypeScriptimport * 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;
}
}
}
TypeScriptimport * 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);
}
});
}
}
TypeScriptimport * 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);
}
}
TypeScriptimport * 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);
}
}
TypeScriptimport * 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));
}
}
TypeScriptimport { 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();
}
}
TypeScriptimport { 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));
}
}
TypeScriptimport { 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);
}
}
TypeScriptimport { 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));
}
}
HTML<dx-dashboard-control
style="display: block;width:100%;height:800px;"
endpoint="http://localhost:5000/api/dashboard"
(onBeforeRender)="onBeforeRender($event)">
</dx-dashboard-control>