diff --git a/application/Espo/Resources/metadata/app/jsLibs.json b/application/Espo/Resources/metadata/app/jsLibs.json
index 734230addd..6372f9e40b 100644
--- a/application/Espo/Resources/metadata/app/jsLibs.json
+++ b/application/Espo/Resources/metadata/app/jsLibs.json
@@ -4,6 +4,11 @@
"exportsTo": "window",
"exportsAs": "Flotr"
},
+ "espo-funnel-chart": {
+ "path": "client/lib/espo-funnel-chart.js",
+ "exportsTo": "window",
+ "exportsAs": "EspoFunnel"
+ },
"Summernote": {
"path": "client/lib/summernote.min.js",
"exportsTo": "$",
diff --git a/client/lib/espo-funnel-chart.js b/client/lib/espo-funnel-chart.js
new file mode 100644
index 0000000000..7fc23f7412
--- /dev/null
+++ b/client/lib/espo-funnel-chart.js
@@ -0,0 +1,209 @@
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var EspoFunnel = {};
+(function (EspoFunnel) {
+ EspoFunnel.Funnel = /*#__PURE__*/function () {
+ function Funnel(container, params, dataList) {
+ _classCallCheck(this, Funnel);
+
+ this.params = Object.assign({}, params || {});
+ this.dataList = dataList || [];
+ this.element = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+
+ if (container) {
+ container.appendChild(this.element);
+ }
+
+ this.trapElementList = [];
+ this.element.setAttribute('width', '100%');
+ this.element.setAttribute('height', '100%');
+ var defaultParams = {
+ padding: 10,
+ colors: ['#b770e0', '#63a7c2', '#c96947', '#ccc058', '#ab414a'],
+ outlineColor: 'red',
+ strokeWidth: 1,
+ gapWidth: 0.01,
+ events: {},
+ showTooltip: true,
+ tooltipClassName: '',
+ tootlipStyleString: 'display:block;position:absolute;white-space:nowrap;',
+ callbacks: {}
+ };
+
+ for (var param in defaultParams) {
+ if (param in this.params) continue;
+ this.params[param] = defaultParams[param];
+ }
+
+ this.draw();
+ }
+
+ _createClass(Funnel, [{
+ key: "getMaxValue",
+ value: function getMaxValue() {
+ var maxValue = 0;
+
+ for (var item of this.dataList) {
+ if (item.value > maxValue) {
+ maxValue = item.value;
+ }
+ }
+
+ return maxValue;
+ }
+ }, {
+ key: "draw",
+ value: function draw() {
+ var cWidth = this.element.getBoundingClientRect().width;
+ var cHeight = this.element.getBoundingClientRect().height;
+ var padding = this.params.padding;
+ var gapWidth = cHeight * this.params.gapWidth;
+ var count = this.dataList.length;
+ this.cWidth = cWidth;
+ var maxValue = this.getMaxValue();
+ var centerX = this.centerX = cWidth / 2;
+ var top = padding;
+ var bottom = height - padding;
+ var width = cWidth - 2 * padding;
+ var height = cHeight - 2 * padding;
+ var itemHeight = (height - gapWidth * count) / count;
+ var ratio = this.ratio = width / maxValue;
+ this.trapElementList.forEach(function (element) {
+ this.element.removeChild(element);
+ }, this);
+ this.trapElementList = [];
+ this.positionList = [];
+ this.dataList.forEach(function (item, i) {
+ var value = item.value;
+ var halfTopWidth = value / 2 * ratio;
+ var iTop = top + itemHeight * i + gapWidth * i;
+ var iBottom = iTop + itemHeight;
+
+ if (i === this.dataList.length - 1) {
+ var nextValue = value;
+ } else {
+ var nextItem = this.dataList[i + 1];
+ var nextValue = nextItem.value;
+ }
+
+ var halfBottomWidth = nextValue / 2 * ratio;
+ this.positionList.push([iTop, iBottom, centerX, halfTopWidth, halfBottomWidth]);
+ var trapElement = this.drawTrapElement(iTop, iBottom, centerX, halfTopWidth, halfBottomWidth, this.getItemColor(i));
+ this.trapElementList.push(trapElement);
+ this.registerMouseEvents(trapElement, i);
+ this.element.appendChild(trapElement);
+ }, this);
+ }
+ }, {
+ key: "getItemColor",
+ value: function getItemColor(i) {
+ var color = this.params.colors[i] || '#AAA';
+ return color;
+ }
+ }, {
+ key: "drawTrapElement",
+ value: function drawTrapElement(iTop, iBottom, centerX, halfTopWidth, halfBottomWidth, color) {
+ var element = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ var d = 'M' + (centerX + halfTopWidth) + ',' + iTop + ' ';
+ d += 'L' + (centerX - halfTopWidth) + ',' + iTop + ' ';
+ d += 'L' + (centerX - halfBottomWidth) + ',' + iBottom + ' ';
+ d += 'L' + (centerX + halfBottomWidth) + ',' + iBottom + ' ';
+ d += 'L' + (centerX + halfTopWidth) + ',' + iTop + '';
+ element.setAttribute('d', d);
+ element.setAttribute('fill', color);
+ element.setAttribute('stroke-width', this.params.strokeWidth);
+ element.setAttribute('stroke', color);
+ return element;
+ }
+ }, {
+ key: "showTooltip",
+ value: function showTooltip(index) {
+ var style = this.params.tootlipStyleString;
+ var tooltipClassName = this.params.tooltipClassName;
+ var element = document.createElement('div');
+ element.setAttribute('style', style);
+ element.setAttribute('class', tooltipClassName);
+ var c = this.params.callbacks.tooltipHtml;
+ if (!c) return;
+ var html = c(index);
+ var pos = this.positionList[index];
+ var left = pos[2] + pos[3];
+ var top = pos[0];
+ var toLeft = pos[3] > this.cWidth / 5;
+ var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
+ var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft;
+ var cLeft = scrollLeft + this.element.getBoundingClientRect().left;
+ var cTop = scrollTop + this.element.getBoundingClientRect().top;
+ left += cLeft;
+ top += cTop;
+ element.innerHTML = html;
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ element.style.pointerEvents = 'none';
+ if (toLeft) element.style.transform = 'translate(-100%, 0)';
+ this.tooltipElement = element;
+ document.body.appendChild(element);
+ }
+ }, {
+ key: "hideTooltip",
+ value: function hideTooltip(index) {
+ document.body.removeChild(this.tooltipElement);
+ }
+ }, {
+ key: "outlineItem",
+ value: function outlineItem(index) {
+ var element = this.trapElementList[index];
+ if (!element) return;
+ element.setAttribute('stroke', this.params.outlineColor);
+ }
+ }, {
+ key: "cancelOutlineItem",
+ value: function cancelOutlineItem(index) {
+ var element = this.trapElementList[index];
+ if (!element) return;
+ element.setAttribute('stroke', this.getItemColor(index));
+ }
+ }, {
+ key: "registerMouseEvents",
+ value: function registerMouseEvents(element, i) {
+ element.onclick = function (e) {
+ if (e.which === 1) {
+ this.triggerEvent('leftClick', {
+ index: i,
+ originalEvent: e
+ });
+ } else if (e.which === 2) {
+ this.triggerEvent('rightClick', {
+ index: i,
+ originalEvent: e
+ });
+ }
+ }.bind(this);
+
+ element.onmouseover = function (e) {
+ this.outlineItem(i);
+ if (this.params.showTooltip) this.showTooltip(i);
+ }.bind(this);
+
+ element.onmouseout = function (e) {
+ this.cancelOutlineItem(i);
+ if (this.params.showTooltip) this.hideTooltip(i);
+ }.bind(this);
+ }
+ }, {
+ key: "triggerEvent",
+ value: function triggerEvent(name, o) {
+ var c = this.params.events[name];
+ if (!c) return;
+ c.call(this, o);
+ }
+ }]);
+
+ return Funnel;
+ }();
+}).call(this, EspoFunnel);
+//# sourceMappingURL=espo-funnel-chart.js.map
diff --git a/client/modules/crm/src/views/dashlets/sales-pipeline.js b/client/modules/crm/src/views/dashlets/sales-pipeline.js
index 423cbdb129..3f64da33ae 100644
--- a/client/modules/crm/src/views/dashlets/sales-pipeline.js
+++ b/client/modules/crm/src/views/dashlets/sales-pipeline.js
@@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
-define('crm:views/dashlets/sales-pipeline', 'crm:views/dashlets/abstract/chart', function (Dep) {
+define('crm:views/dashlets/sales-pipeline', ['crm:views/dashlets/abstract/chart', 'lib!espo-funnel-chart'], function (Dep) {
return Dep.extend({
@@ -55,42 +55,20 @@ define('crm:views/dashlets/sales-pipeline', 'crm:views/dashlets/abstract/chart',
},
prepareData: function (response) {
- var d = [];
+ var list = [];
this.isEmpty = true;
response.dataList.forEach(function (item) {
if (item.value) this.isEmpty = false;
- d.push({
+ list.push({
stageTranslated: this.getLanguage().translateOption(item.stage, 'stage', 'Opportunity'),
value: item.value,
- stage: item.stage
+ stage: item.stage,
});
}, this);
- var data = [];
- for (var i = 0; i < d.length; i++) {
- var item = d[i];
- var value = item.value;
- var nextValue = ((i + 1) < d.length) ? d[i + 1].value : value;
- data.push({
- data: [[i, value], [i + 1, nextValue]],
- label: item.stageTranslated,
- stage: item.stage
- });
- }
-
- var max = 0;
- if (d.length) {
- d.forEach(function (item) {
- if ( item.value && item.value > max) {
- max = item.value;
- }
- }, this);
- }
- this.max = max;
-
- return data;
+ return list;
},
setup: function () {
@@ -112,73 +90,63 @@ define('crm:views/dashlets/sales-pipeline', 'crm:views/dashlets/abstract/chart',
if (this.chartData.length == i + 1 && item.stage === 'Closed Won') {
colors[i] = this.successColor;
}
+ this.chartData[i].color = colors[i];
}, this);
+ this.$container.empty();
- this.flotr.draw(this.$container.get(0), this.chartData, {
- colors: colors,
- shadowSize: false,
- lines: {
- show: true,
- fill: true,
- fillOpacity: 1
+ var funnel = new EspoFunnel.Funnel(
+ this.$container.get(0),
+ {
+ colors: colors,
+ callbacks: {
+ tooltipHtml: function (i) {
+ var value = self.chartData[i].value;
+ return self.chartData[i].stageTranslated + '
' + self.currencySymbol + self.formatNumber(value, true)
+ },
+ },
+ tooltipClassName: 'flotr-mouse-value',
+ tootlipStyleString:
+ 'opacity:0.7;background-color:#000;color:#fff;position:absolute;'+
+ 'padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;',
},
- points: {
- show: true
- },
- grid: {
- color: this.tickColor,
- verticalLines: false,
- outline: '',
- tickColor: this.tickColor
- },
- yaxis: {
- min: 0,
- max: this.max + 0.08 * this.max,
- showLabels: true,
- color: this.textColor,
- tickFormatter: function (value) {
- if (value == 0) {
- return '';
- }
+ this.chartData
+ );
- if (value % 1 == 0) {
- return self.currencySymbol + self.formatNumber(Math.floor(value), false, true).toString();
- }
- return '';
- }
- },
- xaxis: {
- min: 0,
- showLabels: false
- },
- mouse: {
- track: true,
- relative: true,
- position: 'n',
- lineColor: this.hoverColor,
- trackFormatter: function (obj) {
- if (obj.x >= self.chartData.length) {
- return null;
- }
- var label = self.chartData[parseInt(obj.x)].label;
- var label = (label || self.translate('None'));
- return label + '
' + self.currencySymbol + self.formatNumber(obj.y, true);
- }
- },
- legend: {
- show: true,
- noColumns: this.getLegendColumnNumber(),
- container: this.$el.find('.legend-container'),
- labelBoxMargin: 0,
- labelFormatter: self.labelFormatter.bind(self),
- labelBoxBorderColor: 'transparent',
- backgroundOpacity: 0
- }
- });
+ this.drawLegend();
this.adjustLegend();
- }
+ },
+
+ drawLegend: function () {
+ var number = this.getLegendColumnNumber();
+
+ var $container = this.$el.find('.legend-container');
+
+ var html = '
| '+box+' | '; + + html += ''+ + item.stageTranslated+' | '; + }, this); + + html += '