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 = ''; + + this.chartData.forEach(function (item, i) { + if (i % number == 0) { + if (i > 0) html += ''; + html += ''; + } + + var box = '
'+ + '
'+ + '
'; + + html += ''; + + html += ''; + }, this); + + html += ''; + + html += '
'+box+''+ + item.stageTranslated+'
'; + + $container.html(html); + }, }); });