Skip to content

Commit 536fc90

Browse files
start adding funnel item
1 parent 1c75a87 commit 536fc90

File tree

4 files changed

+311
-1
lines changed

4 files changed

+311
-1
lines changed

CS/AspNetCoreCustomItem/Pages/_Layout.cshtml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@
3131
<script src="js/GanttItem.js"></script>
3232
<script src="js/ParameterItem.js"></script>
3333
<script src="js/HierarchicalTreeViewItem.js"></script>
34+
<script src="js/FunnelD3.js"></script>
3435
</body>
3536
</html>

CS/AspNetCoreCustomItem/bundleconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"node_modules/devextreme/dist/js/dx.all.js",
2727
"node_modules/@devexpress/analytics-core/dist/js/dx-analytics-core.min.js",
2828
"node_modules/@devexpress/analytics-core/dist/js/dx-querybuilder.min.js",
29-
"node_modules/devexpress-dashboard/dist/js/dx-dashboard.min.js"
29+
"node_modules/devexpress-dashboard/dist/js/dx-dashboard.min.js",
30+
"node_modules/d3-funnel/dist/js/d3-funnel.min.js"
3031
],
3132
"minify": {
3233
"enabled": false

CS/AspNetCoreCustomItem/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"private": true,
55
"dependencies": {
66
"@devexpress/analytics-core": "^22.1.3",
7+
"d3-funnel": "^2.1.1",
78
"devexpress-dashboard": "^22.1.3",
89
"devexpress-gantt": "3.1.23",
910
"devextreme": "^22.1.3",
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
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

Comments
 (0)