funnel chart
This commit is contained in:
@@ -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": "$",
|
||||
|
||||
@@ -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
|
||||
@@ -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 + '<br>' + 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 + '<br>' + 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 = '<table style="font-size: smaller; color:'+this.textColor+'">';
|
||||
|
||||
this.chartData.forEach(function (item, i) {
|
||||
if (i % number == 0) {
|
||||
if (i > 0) html += '</tr>';
|
||||
html += '<tr>';
|
||||
}
|
||||
|
||||
var box = '<div style="border:1px solid transparent;padding:1px">'+
|
||||
'<div style="width:13px;height:9px;border:1px solid '+item.color+'">'+
|
||||
'<div style="width:14px;height:10px;background-color:'+item.color+';"></div></div></div>';
|
||||
|
||||
html += '<td class="flotr-legend-color-box">'+box+'</td>';
|
||||
|
||||
html += '<td class="flotr-legend-label"><span title="'+item.stageTranslated+'">'+
|
||||
item.stageTranslated+'</span></td>';
|
||||
}, this);
|
||||
|
||||
html += '</tr>';
|
||||
|
||||
html += '</table>';
|
||||
|
||||
$container.html(html);
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user