|
| 1 | +let FunnelD3CustomItem = (function () { |
| 2 | + const Dashboard = DevExpress.Dashboard; |
| 3 | + const Designer = DevExpress.Dashboard.Designer; |
| 4 | + const Model = DevExpress.Dashboard.Model; |
| 5 | + const D3Funnel = D3Funnel; |
| 6 | + |
| 7 | + const FUNNEL_D3_EXTENSION_NAME = 'FunnelD3'; |
| 8 | + 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>'; |
| 9 | + |
| 10 | + const funnelMeta = { |
| 11 | + bindings: [{ |
| 12 | + propertyName: 'Values', |
| 13 | + dataItemType: 'Measure', |
| 14 | + array: true, |
| 15 | + enableColoring: true, |
| 16 | + displayName: 'Values', |
| 17 | + emptyPlaceholder: 'Set Value', |
| 18 | + selectedPlaceholder: 'Configure Value' |
| 19 | + }, { |
| 20 | + propertyName: 'Arguments', |
| 21 | + dataItemType: 'Dimension', |
| 22 | + array: true, |
| 23 | + enableInteractivity: true, |
| 24 | + enableColoring: true, |
| 25 | + displayName: 'Arguments', |
| 26 | + emptyPlaceholder: 'Set Argument', |
| 27 | + selectedPlaceholder: 'Configure Argument' |
| 28 | + }], |
| 29 | + customProperties: [{ |
| 30 | + ownerType: Model.CustomItem, |
| 31 | + propertyName: 'FillType', |
| 32 | + valueType: 'string', |
| 33 | + defaultValue: 'Solid', |
| 34 | + }, { |
| 35 | + ownerType: Model.CustomItem, |
| 36 | + propertyName: 'IsCurved', |
| 37 | + valueType: 'boolean', |
| 38 | + defaultValue: false, |
| 39 | + }, { |
| 40 | + ownerType: Model.CustomItem, |
| 41 | + propertyName: 'IsDynamicHeight', |
| 42 | + valueType: 'boolean', |
| 43 | + defaultValue: true, |
| 44 | + }, { |
| 45 | + ownerType: Model.CustomItem, |
| 46 | + propertyName: 'PinchCount', |
| 47 | + valueType: 'number', |
| 48 | + defaultValue: 0, |
| 49 | + }], |
| 50 | + optionsPanelSections: [{ |
| 51 | + title: 'Settings', |
| 52 | + items: [{ |
| 53 | + dataField: 'FillType', |
| 54 | + template: Designer.FormItemTemplates.buttonGroup, |
| 55 | + editorOptions: { |
| 56 | + items: [{ text: 'Solid' }, { text: 'Gradient' }] |
| 57 | + }, |
| 58 | + }, { |
| 59 | + dataField: 'IsCurved', |
| 60 | + label: { |
| 61 | + text: 'Curved' |
| 62 | + }, |
| 63 | + template: Designer.FormItemTemplates.buttonGroup, |
| 64 | + editorOptions: { |
| 65 | + keyExpr: 'value', |
| 66 | + items: [{ |
| 67 | + value: false, |
| 68 | + text: 'No', |
| 69 | + }, { |
| 70 | + value: true, |
| 71 | + text: 'Yes', |
| 72 | + }] |
| 73 | + }, |
| 74 | + }, { |
| 75 | + dataField: 'IsDynamicHeight', |
| 76 | + label: { |
| 77 | + text: 'Dynamic Height' |
| 78 | + }, |
| 79 | + template: Designer.FormItemTemplates.buttonGroup, |
| 80 | + editorOptions: { |
| 81 | + keyExpr: 'value', |
| 82 | + items: [{ |
| 83 | + value: false, |
| 84 | + text: 'No', |
| 85 | + }, { |
| 86 | + value: true, |
| 87 | + text: 'Yes', |
| 88 | + }] |
| 89 | + }, |
| 90 | + }, { |
| 91 | + dataField: 'PinchCount', |
| 92 | + editorType: 'dxNumberBox', |
| 93 | + editorOptions: { |
| 94 | + min: 0, |
| 95 | + }, |
| 96 | + }], |
| 97 | + }], |
| 98 | + interactivity: { |
| 99 | + filter: true, |
| 100 | + drillDown: true |
| 101 | + }, |
| 102 | + icon: FUNNEL_D3_EXTENSION_NAME, |
| 103 | + title: 'Funnel D3', |
| 104 | + }; |
| 105 | + |
| 106 | + class FunnelD3ItemViewer extends Dashboard.CustomItemViewer { |
| 107 | + funnelSettings; |
| 108 | + funnelViewer; |
| 109 | + selectionValues; |
| 110 | + exportingImage; |
| 111 | + funnelContainer; |
| 112 | + |
| 113 | + constructor(model, container, options) { |
| 114 | + super(model, container, options); |
| 115 | + |
| 116 | + this.funnelSettings = undefined; |
| 117 | + this.funnelViewer = null; |
| 118 | + this.selectionValues = []; |
| 119 | + this.exportingImage = new Image(); |
| 120 | + this._subscribeProperties(); |
| 121 | + } |
| 122 | + |
| 123 | + renderContent(element, changeExisting) { |
| 124 | + let htmlElement = element; |
| 125 | + |
| 126 | + var data = this._getDataSource(); |
| 127 | + if (!this._ensureFunnelLibrary(htmlElement)) |
| 128 | + return; |
| 129 | + if (!!data) { |
| 130 | + if (!changeExisting || !this.funnelViewer) { |
| 131 | + while (htmlElement.firstChild) |
| 132 | + htmlElement.removeChild(htmlElement.firstChild); |
| 133 | + |
| 134 | + this.funnelContainer = document.createElement('div'); |
| 135 | + this.funnelContainer.style.margin = '20px'; |
| 136 | + this.funnelContainer.style.height = 'calc(100% - 40px)'; |
| 137 | + |
| 138 | + htmlElement.appendChild(this.funnelContainer); |
| 139 | + this.funnelViewer = new D3Funnel(this.funnelContainer); |
| 140 | + } |
| 141 | + this._update(data, this._getFunnelSizeOptions()); |
| 142 | + } else { |
| 143 | + while (htmlElement.firstChild) |
| 144 | + htmlElement.removeChild(htmlElement.firstChild); |
| 145 | + |
| 146 | + this.funnelViewer = null; |
| 147 | + } |
| 148 | + } |
| 149 | + setSize(width, height) { |
| 150 | + super.setSize(width, height); |
| 151 | + this._update(null, this._getFunnelSizeOptions()); |
| 152 | + } |
| 153 | + setSelection(values) { |
| 154 | + super.setSelection(values); |
| 155 | + this._update(this._getDataSource()); |
| 156 | + } |
| 157 | + clearSelection() { |
| 158 | + super.clearSelection(); |
| 159 | + this._update(this._getDataSource()); |
| 160 | + } |
| 161 | + allowExportSingleItem() { |
| 162 | + return !this._isIEBrowser(); |
| 163 | + } |
| 164 | + getExportInfo() { |
| 165 | + return { |
| 166 | + image: this._isIEBrowser() ? '' : this._getImageBase64() |
| 167 | + }; |
| 168 | + } |
| 169 | + _getFunnelSizeOptions() { |
| 170 | + if (!this.funnelContainer) |
| 171 | + return {}; |
| 172 | + |
| 173 | + return { chart: { width: this.funnelContainer.clientWidth, height: this.funnelContainer.clientHeight } }; |
| 174 | + } |
| 175 | + _getDataSource() { |
| 176 | + var bindingValues = this.getBindingValue('Values'); |
| 177 | + if (bindingValues.length == 0) |
| 178 | + return undefined; |
| 179 | + var data = []; |
| 180 | + this.iterateData((dataRow) => { |
| 181 | + var values = dataRow.getValue('Values'); |
| 182 | + var valueStr = dataRow.getDisplayText('Values'); |
| 183 | + var color = dataRow.getColor('Values'); |
| 184 | + if (this._hasArguments()) { |
| 185 | + var labelText = dataRow.getDisplayText('Arguments').join(' - ') + ': ' + valueStr; |
| 186 | + data.push([{ data: dataRow, text: labelText, color: color[0] }].concat(values));//0 - 'layer' index for color value |
| 187 | + } else { |
| 188 | + data = values.map((value, index) => { return [{ text: bindingValues[index].displayName() + ': ' + valueStr[index], color: color[index] }, value]; }); |
| 189 | + } |
| 190 | + }); |
| 191 | + return data.length > 0 ? data : undefined; |
| 192 | + } |
| 193 | + _ensureFunnelLibrary(htmlElement) { |
| 194 | + if (!D3Funnel) { |
| 195 | + htmlElement.innerHTML = ''; |
| 196 | + var textDiv = document.createElement('div'); |
| 197 | + textDiv.style.position = 'absolute'; |
| 198 | + textDiv.style.top = '50%'; |
| 199 | + textDiv.style.transform = 'translateY(-50%)'; |
| 200 | + textDiv.style.width = '95%'; |
| 201 | + textDiv.style.color = '#CF0F2E'; |
| 202 | + textDiv.style.textAlign = 'center'; |
| 203 | + textDiv.innerText = "'D3Funnel' cannot be displayed. You should include 'd3.v3.min.js' and 'd3-funnel.js' libraries."; |
| 204 | + htmlElement.appendChild(textDiv); |
| 205 | + return false; |
| 206 | + } |
| 207 | + return true; |
| 208 | + } |
| 209 | + _ensureFunnelSettings() { |
| 210 | + |
| 211 | + var getSelectionColor = (hexColor) => { return this.funnelViewer.colorizer.shade(hexColor, -0.5); }; |
| 212 | + if (!this.funnelSettings) { |
| 213 | + this.funnelSettings = { |
| 214 | + data: undefined, |
| 215 | + options: { |
| 216 | + chart: { |
| 217 | + bottomPinch: this.getPropertyValue('PinchCount'), |
| 218 | + curve: { enabled: this.getPropertyValue('IsCurved') } |
| 219 | + }, |
| 220 | + block: { |
| 221 | + dynamicHeight: this.getPropertyValue('IsDynamicHeight'), |
| 222 | + fill: { |
| 223 | + scale: (index) => { |
| 224 | + var obj = this.funnelSettings.data[index][0]; |
| 225 | + return obj.data && this.isSelected(obj.data) ? getSelectionColor(obj.color) : obj.color; |
| 226 | + }, |
| 227 | + type: (this.getPropertyValue('FillType')).toLowerCase() |
| 228 | + } |
| 229 | + }, |
| 230 | + label: { |
| 231 | + format: (label, value) => { |
| 232 | + return label.text; |
| 233 | + } |
| 234 | + }, |
| 235 | + events: { |
| 236 | + click: { block: (e) => this._onClick(e) } |
| 237 | + } |
| 238 | + } |
| 239 | + }; |
| 240 | + } |
| 241 | + this.funnelSettings.options.block.highlight = this.canDrillDown() || this.canMasterFilter(); |
| 242 | + return this.funnelSettings; |
| 243 | + } |
| 244 | + _onClick(e) { |
| 245 | + if (!this._hasArguments() || !e.label) |
| 246 | + return; |
| 247 | + var row = e.label.raw.data; |
| 248 | + if (this.canDrillDown(row)) |
| 249 | + this.drillDown(row); |
| 250 | + else if (this.canMasterFilter(row)) { |
| 251 | + this.setMasterFilter(row); |
| 252 | + this._update(); |
| 253 | + } |
| 254 | + } |
| 255 | + _subscribeProperties() { |
| 256 | + this.subscribe('IsCurved', (isCurved) => this._update(null, { chart: { curve: { enabled: isCurved } } })); |
| 257 | + this.subscribe('IsDynamicHeight', (isDynamicHeight) => this._update(null, { block: { dynamicHeight: isDynamicHeight } })); |
| 258 | + this.subscribe('PinchCount', (count) => this._update(null, { chart: { bottomPinch: count } })); |
| 259 | + this.subscribe('FillType', (type) => this._update(null, { block: { fill: { type: type.toLowerCase() } } })); |
| 260 | + } |
| 261 | + _update(data, options) { |
| 262 | + this._ensureFunnelSettings(); |
| 263 | + if (!!data) { |
| 264 | + this.funnelSettings.data = data; |
| 265 | + } |
| 266 | + if (!!options) { |
| 267 | + this.funnelSettings.options = { ...this.funnelSettings.options, ...options }; |
| 268 | + } |
| 269 | + if (!!this.funnelViewer) { |
| 270 | + this.funnelViewer.draw(this.funnelSettings.data, this.funnelSettings.options); |
| 271 | + this._updateExportingImage(); |
| 272 | + } |
| 273 | + } |
| 274 | + _updateExportingImage() { |
| 275 | + const svg = this.funnelContainer.firstElementChild; |
| 276 | + const str = new XMLSerializer().serializeToString(svg), |
| 277 | + encodedData = 'data:image/svg+xml;base64,' + window.btoa(window['unescape'](encodeURIComponent(str))); |
| 278 | + this.exportingImage.src = encodedData; |
| 279 | + } |
| 280 | + _hasArguments() { |
| 281 | + return this.getBindingValue('Arguments').length > 0; |
| 282 | + } |
| 283 | + _getImageBase64() { |
| 284 | + let canvas = document.createElement('canvas'); |
| 285 | + canvas.width = this.funnelContainer.clientWidth; |
| 286 | + canvas.height = this.funnelContainer.clientHeight; |
| 287 | + const canvasContext = canvas.getContext('2d'); |
| 288 | + canvasContext && canvasContext.drawImage(this.exportingImage, 0, 0); |
| 289 | + return canvas.toDataURL().replace('data:image/png;base64,', ''); |
| 290 | + } |
| 291 | + _isIEBrowser() { |
| 292 | + return navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0; |
| 293 | + } |
| 294 | + } |
| 295 | + |
| 296 | + function FunnelD3Item(dashboardControl) { |
| 297 | + dashboardControl.registerIcon(svgIcon); |
| 298 | + this.name = FUNNEL_D3_EXTENSION_NAME; |
| 299 | + this.metaData = funnelMeta; |
| 300 | + this.createViewerItem = function (model, $element, content) { |
| 301 | + return new FunnelD3ItemViewer(model, $element, content); |
| 302 | + } |
| 303 | + }; |
| 304 | + |
| 305 | + return FunnelD3Item; |
| 306 | +})(); |
| 307 | + |
0 commit comments