Skip to main content

Create a Data-Aware Item Based on an External Visualization Widget for the Web Dashboard

  • 14 minutes to read

Important

A custom item is an independent module called an extension. If you are not familiar with the basic concepts of extensions, please review the following topic before you continue: Extensions Overview.

In the previous lesson, you created a custom item that displays formatted dimension values and allows users to color these values.

This tutorial shows you how to create a more “real-world” custom item that uses an external visualization widget. In this example, it is the dxFunnel widget that can show values across multiple stages in a process. This widget supports data binding, master filtering, and coloring.

You can download the ready-to-use projects based on the Create a custom item tutorials:

View Example: ASP.NET Core View Example: React

Prerequisites

A custom item script depends on the platform you use:

Module script
Client components (Angular, React, Vue) use imports and classes to define a custom item.
Classic script
A script for the ASP.NET Web Forms, ASP.NET MVC, ASP.NET Core, BLazor, and JavaScript controls encapsulates a custom item in the IIFE function expression.

Before you begin, create a Web Dashboard project for the required platform.

Create an External Script

In your project, create an external FunnelChartItem.js file, so you can reuse the created extension in other Web Dashboard applications.

Define a class that will be a custom item (FunnelChartItem).

import { Dashboard, CustomItem } from 'devexpress-dashboard/model'
import { CustomItemViewer, ResourceManager } from 'devexpress-dashboard/common'
import { FormItemTemplates } from 'devexpress-dashboard/designer'
import dxFunnel from 'devextreme/viz/funnel'
class FunnelChartItem {

export default FunnelChartItem;

Warning

If you use the Classic script, place the code below inside the self-invoking function.

Define a Toolbox Icon

In the FunnelChartItem.js file, add a 24x24 SVG icon as a string variable:

var svgIcon = '<svg id="funnelChartItemIcon" viewBox="0 0 24 24"><path stroke="#ffffff" fill="#f442ae" d="M12 2 L2 22 L22 22 Z" /></svg>';

The icon will be used to display the custom item in the Toolbox:

CustomItem_Funnel_Toolbox

Specify the Custom Item’s Settings

Implement the ICustomItemMetaData interface to define this custom item’s settings:

Use the ICustomItemMetaData.customProperties and ICustomItemMetaData.optionsPanelSections properties to define a custom option that allows users to change the label position:

var funnelChartItemMetaData = {
    bindings: [{
        propertyName: 'measureValue',
        dataItemType: 'Measure',
        displayName: 'Value'
    }, {
        propertyName: 'dimensionValue',
        dataItemType: 'Dimension',
        displayName: 'Argument',
        enableColoring: true,
        enableInteractivity: true
    }],
    interactivity: {
        filter: true
    },
    customProperties: [{
        ownerType: CustomItem,
        propertyName: 'labelPositionProperty',
        valueType: 'string',
        defaultValue: 'Inside'
    }],
    optionsPanelSections: [{
        title: 'Labels',
        items: [{
            dataField: 'labelPositionProperty',
            label: {
                text: 'Label Position'
            },
            template: FormItemTemplates.buttonGroup,
            editorOptions: {
                items: [{ text: 'Inside' }, { text: 'Outside' }]
            }
        }]
    }],
    icon: 'funnelChartItemIcon',
    title: 'Funnel Chart'
};

Implement Custom Item Rendering

The CustomItemViewer class specifies visual representation of the custom item. Override the CustomItemViewer.renderContent method to display custom item content. In the CustomItemViewer.setSize method, specify the correct widget size at runtime if the actual size is not known beforehand (SVG-widgets); this will allow you to scale the custom item when a user changes the item’s size.

Create methods that allow you to display measure and dimension values from a data source and implement the following logic for a custom property:

_getDataSource
The _getDataSource method returns data displayed in the custom item. The CustomItemViewer.iterateData method generates data rows. When a user binds this custom item to a data field and enables coloring, the iterateData method is called for each row in aggregated data. The iterateData method accepts a function as a parameter, and allows you to iterate through aggregated data and to obtain the required values (dimension or measure values, display text, colors, and more). See ICustomDataRow methods to learn more about the kind of data row information you can obtain.
_getDxFunnelWidgetSettings
The _getDxFunnelWidgetSettings method returns widget settings when you render custom item content. The Options | Labels | Label Position custom UI option defines the label position. Use the CustomItemViewer.getPropertyValue method to get the value of the created custom option and to change the label position according to the returned value.

Note

Do not set IDs for DOM elements in the CustomItemViewer.renderContent method call. When the custom item is maximized, the renderContent method is called again and creates one more DOM element with the same ID. If you create an element inside this method, the element ID will not be unique on the page.

import { Dashboard, CustomItem } from 'devexpress-dashboard/model'
import { CustomItemViewer, ResourceManager } from 'devexpress-dashboard/common'
import { FormItemTemplates } from 'devexpress-dashboard/designer'
import dxFunnel from 'devextreme/viz/funnel'
// ...
class FunnelChartItemViewer extends CustomItemViewer {
    constructor(model, $container, options) {       
        super(model, $container, options);
        this.dxFunnelWidget = null;
        this.dxFunnelWidgetSettings = undefined;
    }

    _getDataSource() {
        var clientData = [];
        if (this.getBindingValue('measureValue').length > 0) {
            this.iterateData(function (dataRow) {
                clientData.push({
                    measureValue: dataRow.getValue('measureValue')[0],
                    dimensionValue: dataRow.getValue('dimensionValue')[0] || '',
                    dimensionDisplayText: dataRow.getDisplayText('dimensionValue')[0],
                    measureDisplayText: dataRow.getDisplayText('measureValue')[0],
                    dimensionColor: dataRow.getColor('dimensionValue')[0],
                    clientDataRow: dataRow
                });
            });
        }
        return clientData;
    };

    _getDxFunnelWidgetSettings() {
        var _this = this;
        return {
            dataSource: this._getDataSource(),
            argumentField: "dimensionValue",
            valueField: "measureValue",
            colorField: "dimensionColor",
            selectionMode: "multiple",
            label: {
                customizeText: function (e) {
                    return e.item.data.dimensionDisplayText + ': ' + e.item.data.measureDisplayText;
                },
                position: this.getPropertyValue('labelPositionProperty').toLowerCase()
            },
            // ...
        };
    }

// ...
    setSize(width, height) {
        Object.getPrototypeOf(FunnelChartItemViewer.prototype).setSize.call(this, width, height);
        this.dxFunnelWidget.render();
    }

    renderContent(element, changeExisting) {
        if (!changeExisting) {

            while (element.firstChild)
                element.removeChild(element.firstChild);

            var div = document.createElement('div');
            div.style.width = "100%";
            div.style.height = "100%";
            element.appendChild(div);
            this.dxFunnelWidget = new dxFunnel(div, this._getDxFunnelWidgetSettings());
        } else {
            this.dxFunnelWidget.option(this._getDxFunnelWidgetSettings());
        }
    }
}

Manage Interactivity

To support Master-Filtering, override the following methods and describe this custom item’s behavior when a user selects its data row:

import { Dashboard, CustomItem } from 'devexpress-dashboard/model'
import { CustomItemViewer, ResourceManager } from 'devexpress-dashboard/common'
import { FormItemTemplates } from 'devexpress-dashboard/designer'
import dxFunnel from 'devextreme/viz/funnel'
// ...
class FunnelChartItemViewer extends CustomItemViewer {
// ...
    _getDxFunnelWidgetSettings() {
        var _this = this;
        return {
        // ...
            onItemClick: function (e) {
                _this.setMasterFilter(e.item.data.clientDataRow);
            }
        };
    }

    setSelection() {
        var _this = this;
        this.dxFunnelWidget.getAllItems().forEach(function (item) {
            item.select(_this.isSelected(item.data.clientDataRow));
        });
    }

    clearSelection() {
        this.dxFunnelWidget.clearSelection();
    }
    // ...
}

Create an Extension

The ICustomItemExtension interface allows you to define a custom item as an extension.

import { Dashboard, CustomItem } from 'devexpress-dashboard/model'
import { CustomItemViewer, ResourceManager } from 'devexpress-dashboard/common'
import { FormItemTemplates } from 'devexpress-dashboard/designer'
import dxFunnel from 'devextreme/viz/funnel'
// ...
class FunnelChartItem {
    constructor(dashboardControl) {
        ResourceManager.registerIcon(svgIcon);
        this.name = "funnelChartCustomItem";
        this.metaData = funnelChartItemMetaData;
    }

    createViewerItem(model, $element, content) {
        return new FunnelChartItemViewer(model, $element, content);
    }
}

export default FunnelChartItem;

The complete extension code:

Show code
// #region imports
import { Dashboard, CustomItem } from 'devexpress-dashboard/model'
import { CustomItemViewer, ResourceManager } from 'devexpress-dashboard/common'
import { FormItemTemplates } from 'devexpress-dashboard/designer'
import dxFunnel from 'devextreme/viz/funnel'
// #endregion
// #region svgIcon
var svgIcon = '<svg id="funnelChartItemIcon" viewBox="0 0 24 24"><path stroke="#ffffff" fill="#f442ae" d="M12 2 L2 22 L22 22 Z" /></svg>';
// #endregion
// #region metadata
var funnelChartItemMetaData = {
  bindings: [{
      propertyName: 'measureValue',
      dataItemType: 'Measure',
      displayName: 'Value'
  }, {
      propertyName: 'dimensionValue',
      dataItemType: 'Dimension',
      displayName: 'Argument',
      enableColoring: true,
      enableInteractivity: true
  }],
  interactivity: {
      filter: true
  },
  customProperties: [{
      ownerType: CustomItem,
      propertyName: 'labelPositionProperty',
      valueType: 'string',
      defaultValue: 'Inside'
  }],
  optionsPanelSections: [{
      title: 'Labels',
      items: [{
          dataField: 'labelPositionProperty',
          label: {
              text: 'Label Position'
          },
          template: FormItemTemplates.buttonGroup,
          editorOptions: {
              items: [{ text: 'Inside' }, { text: 'Outside' }]
          }
      }]
  }],
  icon: 'funnelChartItemIcon',
  title: 'Funnel Chart'
};
// #endregion
// #region viewer
class FunnelChartItemViewer extends CustomItemViewer {
  constructor(model, $container, options) {       
        super(model, $container, options);
        this.dxFunnelWidget = null;
        this.dxFunnelWidgetSettings = undefined;
  }

    _getDataSource() {
        var clientData = [];
        if (this.getBindingValue('measureValue').length > 0) {
            this.iterateData(function (dataRow) {
                clientData.push({
                    measureValue: dataRow.getValue('measureValue')[0],
                    dimensionValue: dataRow.getValue('dimensionValue')[0] || '',
                    dimensionDisplayText: dataRow.getDisplayText('dimensionValue')[0],
                    measureDisplayText: dataRow.getDisplayText('measureValue')[0],
                    dimensionColor: dataRow.getColor('dimensionValue')[0],
                    clientDataRow: dataRow
                });
            });
        }
        return clientData;
    };

    _getDxFunnelWidgetSettings() {
        var _this = this;
        return {
            dataSource: this._getDataSource(),
            argumentField: "dimensionValue",
            valueField: "measureValue",
            colorField: "dimensionColor",
            selectionMode: "multiple",
            label: {
                customizeText: function (e) {
                    return e.item.data.dimensionDisplayText + ': ' + e.item.data.measureDisplayText;
                },
                position: this.getPropertyValue('labelPositionProperty').toLowerCase()
            },
            onItemClick: function (e) {
                _this.setMasterFilter(e.item.data.clientDataRow);
            }
        };
    }

    setSelection() {
        var _this = this;
        this.dxFunnelWidget.getAllItems().forEach(function (item) {
            item.select(_this.isSelected(item.data.clientDataRow));
        });
    }

    clearSelection() {
        this.dxFunnelWidget.clearSelection();
    }

    setSize(width, height) {
        Object.getPrototypeOf(FunnelChartItemViewer.prototype).setSize.call(this, width, height);
        this.dxFunnelWidget.render();
    }

    renderContent(element, changeExisting) {
        if (!changeExisting) {

            while (element.firstChild)
                element.removeChild(element.firstChild);

            var div = document.createElement('div');
            div.style.width = "100%";
            div.style.height = "100%";
            element.appendChild(div);
            this.dxFunnelWidget = new dxFunnel(div, this._getDxFunnelWidgetSettings());
        } else {
            this.dxFunnelWidget.option(this._getDxFunnelWidgetSettings());
        }
    }
}
// #endregion
// #region createItem
class FunnelChartItem {
    constructor(dashboardControl) {
        ResourceManager.registerIcon(svgIcon);
        this.name = "funnelChartCustomItem";
      this.metaData = funnelChartItemMetaData;
    }

    createViewerItem(model, $element, content) {
        return new FunnelChartItemViewer(model, $element, content);
    }
}

export default FunnelChartItem;
// #endregion

Register the Custom Item as an Extension

Attach the created FunnelChartItem.js script file to the page containing the Web Dashboard code. The external script you created is now available in the application.

You need to register the extension before the control is rendered. Refer to the following topics for information on how to get access to the DashboardControl instance and customize it before any element in the Web Dashboard control has been rendered:

Register the created extension in the DashboardControl.registerExtension method call:

function onBeforeRender(e) { 
  var dashboardControl = e.component;
  dashboardControl.registerExtension(new DashboardPanelExtension(dashboardControl));
  dashboardControl.registerExtension(new HelloWorldItem(dashboardControl));
  dashboardControl.registerExtension(new DataAwareItem(dashboardControl));
  dashboardControl.registerExtension(new FunnelChartItem(dashboardControl));
}

Result

Run the project and click the Funnel Chart Toolbox item to add the custom item to the dashboard:

CustomItem_Funnel_Toolbox

This adds the empty item to the dashboard.

CustomItem_Funnel_Empty

Invoke the Binding menu, click Set Value in the Value section, and select a measure. Then, click Set Argument to supply the custom item with argument values:

CustomItem_Funnel_WithoutColoring

Use Options | Coloring to enable coloring:

CustomItem_Funnel_WithColoring

Add another dashboard item (for instance, the previously created Data-Aware Item) and bind it to data. Enable master filtering in the Funnel Chart and select any element to see the result:

CustomItem_Funnel_MasterFiltering

You can change the label position using the Options | Labels | Label Position custom option.

CustomItem_Funnel_LabelPosition

What’s Next

Add Interactivity

You can change the detail level of data displayed in a custom dashboard item or use a custom dashboard item as a filter for other dashboard items. For this, you need to set up custom interactivity. See the following article for details: Custom Interactivity in the Web Dashboard.

Custom Item Export

You can export custom dashboard items to the following formats:

  • Excel (XLS, XLSX)
  • PDF
  • Image

To do this, you need to specify the export logic in the item’s configuration object. See the following topic for this information: Custom Item Export.