blob: 013be3c2876d01ef26dfad1dc23af9ef9b95a796 [file] [log] [blame]
define('echarts/chart/funnel', [
'require',
'./base',
'zrender/shape/Text',
'zrender/shape/Line',
'zrender/shape/Polygon',
'../config',
'../util/ecData',
'../util/number',
'zrender/tool/util',
'zrender/tool/color',
'zrender/tool/area',
'../chart'
], function (require) {
var ChartBase = require('./base');
var TextShape = require('zrender/shape/Text');
var LineShape = require('zrender/shape/Line');
var PolygonShape = require('zrender/shape/Polygon');
var ecConfig = require('../config');
ecConfig.funnel = {
zlevel: 0,
z: 2,
clickable: true,
legendHoverLink: true,
x: 80,
y: 60,
x2: 80,
y2: 60,
min: 0,
max: 100,
minSize: '0%',
maxSize: '100%',
sort: 'descending',
gap: 0,
funnelAlign: 'center',
itemStyle: {
normal: {
borderColor: '#fff',
borderWidth: 1,
label: {
show: true,
position: 'outer'
},
labelLine: {
show: true,
length: 10,
lineStyle: {
width: 1,
type: 'solid'
}
}
},
emphasis: {
borderColor: 'rgba(0,0,0,0)',
borderWidth: 1,
label: { show: true },
labelLine: { show: true }
}
}
};
var ecData = require('../util/ecData');
var number = require('../util/number');
var zrUtil = require('zrender/tool/util');
var zrColor = require('zrender/tool/color');
var zrArea = require('zrender/tool/area');
function Funnel(ecTheme, messageCenter, zr, option, myChart) {
ChartBase.call(this, ecTheme, messageCenter, zr, option, myChart);
this.refresh(option);
}
Funnel.prototype = {
type: ecConfig.CHART_TYPE_FUNNEL,
_buildShape: function () {
var series = this.series;
var legend = this.component.legend;
this._paramsMap = {};
this._selected = {};
this.selectedMap = {};
var serieName;
for (var i = 0, l = series.length; i < l; i++) {
if (series[i].type === ecConfig.CHART_TYPE_FUNNEL) {
series[i] = this.reformOption(series[i]);
this.legendHoverLink = series[i].legendHoverLink || this.legendHoverLink;
serieName = series[i].name || '';
this.selectedMap[serieName] = legend ? legend.isSelected(serieName) : true;
if (!this.selectedMap[serieName]) {
continue;
}
this._buildSingleFunnel(i);
this.buildMark(i);
}
}
this.addShapeList();
},
_buildSingleFunnel: function (seriesIndex) {
var legend = this.component.legend;
var serie = this.series[seriesIndex];
var data = this._mapData(seriesIndex);
var location = this._getLocation(seriesIndex);
this._paramsMap[seriesIndex] = {
location: location,
data: data
};
var itemName;
var total = 0;
var selectedData = [];
for (var i = 0, l = data.length; i < l; i++) {
itemName = data[i].name;
this.selectedMap[itemName] = legend ? legend.isSelected(itemName) : true;
if (this.selectedMap[itemName] && !isNaN(data[i].value)) {
selectedData.push(data[i]);
total++;
}
}
if (total === 0) {
return;
}
var funnelCase = this._buildFunnelCase(seriesIndex);
var align = serie.funnelAlign;
var gap = serie.gap;
var height = total > 1 ? (location.height - (total - 1) * gap) / total : location.height;
var width;
var lastY = location.y;
var lastWidth = serie.sort === 'descending' ? this._getItemWidth(seriesIndex, selectedData[0].value) : number.parsePercent(serie.minSize, location.width);
var next = serie.sort === 'descending' ? 1 : 0;
var centerX = location.centerX;
var pointList = [];
var x;
var polygon;
var lastPolygon;
for (var i = 0, l = selectedData.length; i < l; i++) {
itemName = selectedData[i].name;
if (this.selectedMap[itemName] && !isNaN(selectedData[i].value)) {
width = i <= l - 2 ? this._getItemWidth(seriesIndex, selectedData[i + next].value) : serie.sort === 'descending' ? number.parsePercent(serie.minSize, location.width) : number.parsePercent(serie.maxSize, location.width);
switch (align) {
case 'left':
x = location.x;
break;
case 'right':
x = location.x + location.width - lastWidth;
break;
default:
x = centerX - lastWidth / 2;
}
polygon = this._buildItem(seriesIndex, selectedData[i]._index, legend ? legend.getColor(itemName) : this.zr.getColor(selectedData[i]._index), x, lastY, lastWidth, width, height, align);
lastY += height + gap;
lastPolygon = polygon.style.pointList;
pointList.unshift([
lastPolygon[0][0] - 10,
lastPolygon[0][1]
]);
pointList.push([
lastPolygon[1][0] + 10,
lastPolygon[1][1]
]);
if (i === 0) {
if (lastWidth === 0) {
lastPolygon = pointList.pop();
align == 'center' && (pointList[0][0] += 10);
align == 'right' && (pointList[0][0] = lastPolygon[0]);
pointList[0][1] -= align == 'center' ? 10 : 15;
if (l == 1) {
lastPolygon = polygon.style.pointList;
}
} else {
pointList[pointList.length - 1][1] -= 5;
pointList[0][1] -= 5;
}
}
lastWidth = width;
}
}
if (funnelCase) {
pointList.unshift([
lastPolygon[3][0] - 10,
lastPolygon[3][1]
]);
pointList.push([
lastPolygon[2][0] + 10,
lastPolygon[2][1]
]);
if (lastWidth === 0) {
lastPolygon = pointList.pop();
align == 'center' && (pointList[0][0] += 10);
align == 'right' && (pointList[0][0] = lastPolygon[0]);
pointList[0][1] += align == 'center' ? 10 : 15;
} else {
pointList[pointList.length - 1][1] += 5;
pointList[0][1] += 5;
}
funnelCase.style.pointList = pointList;
}
},
_buildFunnelCase: function (seriesIndex) {
var serie = this.series[seriesIndex];
if (this.deepQuery([
serie,
this.option
], 'calculable')) {
var location = this._paramsMap[seriesIndex].location;
var gap = 10;
var funnelCase = {
hoverable: false,
style: {
pointListd: [
[
location.x - gap,
location.y - gap
],
[
location.x + location.width + gap,
location.y - gap
],
[
location.x + location.width + gap,
location.y + location.height + gap
],
[
location.x - gap,
location.y + location.height + gap
]
],
brushType: 'stroke',
lineWidth: 1,
strokeColor: serie.calculableHolderColor || this.ecTheme.calculableHolderColor || ecConfig.calculableHolderColor
}
};
ecData.pack(funnelCase, serie, seriesIndex, undefined, -1);
this.setCalculable(funnelCase);
funnelCase = new PolygonShape(funnelCase);
this.shapeList.push(funnelCase);
return funnelCase;
}
},
_getLocation: function (seriesIndex) {
var gridOption = this.series[seriesIndex];
var zrWidth = this.zr.getWidth();
var zrHeight = this.zr.getHeight();
var x = this.parsePercent(gridOption.x, zrWidth);
var y = this.parsePercent(gridOption.y, zrHeight);
var width = gridOption.width == null ? zrWidth - x - this.parsePercent(gridOption.x2, zrWidth) : this.parsePercent(gridOption.width, zrWidth);
return {
x: x,
y: y,
width: width,
height: gridOption.height == null ? zrHeight - y - this.parsePercent(gridOption.y2, zrHeight) : this.parsePercent(gridOption.height, zrHeight),
centerX: x + width / 2
};
},
_mapData: function (seriesIndex) {
var serie = this.series[seriesIndex];
var funnelData = zrUtil.clone(serie.data);
for (var i = 0, l = funnelData.length; i < l; i++) {
funnelData[i]._index = i;
}
function numDescending(a, b) {
if (a.value === '-') {
return 1;
} else if (b.value === '-') {
return -1;
}
return b.value - a.value;
}
function numAscending(a, b) {
return -numDescending(a, b);
}
if (serie.sort != 'none') {
funnelData.sort(serie.sort === 'descending' ? numDescending : numAscending);
}
return funnelData;
},
_buildItem: function (seriesIndex, dataIndex, defaultColor, x, y, topWidth, bottomWidth, height, align) {
var series = this.series;
var serie = series[seriesIndex];
var data = serie.data[dataIndex];
var polygon = this.getPolygon(seriesIndex, dataIndex, defaultColor, x, y, topWidth, bottomWidth, height, align);
ecData.pack(polygon, series[seriesIndex], seriesIndex, series[seriesIndex].data[dataIndex], dataIndex, series[seriesIndex].data[dataIndex].name);
this.shapeList.push(polygon);
var label = this.getLabel(seriesIndex, dataIndex, defaultColor, x, y, topWidth, bottomWidth, height, align);
ecData.pack(label, series[seriesIndex], seriesIndex, series[seriesIndex].data[dataIndex], dataIndex, series[seriesIndex].data[dataIndex].name);
this.shapeList.push(label);
if (!this._needLabel(serie, data, false)) {
label.invisible = true;
}
var labelLine = this.getLabelLine(seriesIndex, dataIndex, defaultColor, x, y, topWidth, bottomWidth, height, align);
this.shapeList.push(labelLine);
if (!this._needLabelLine(serie, data, false)) {
labelLine.invisible = true;
}
var polygonHoverConnect = [];
var labelHoverConnect = [];
if (this._needLabelLine(serie, data, true)) {
polygonHoverConnect.push(labelLine.id);
labelHoverConnect.push(labelLine.id);
}
if (this._needLabel(serie, data, true)) {
polygonHoverConnect.push(label.id);
labelHoverConnect.push(polygon.id);
}
polygon.hoverConnect = polygonHoverConnect;
label.hoverConnect = labelHoverConnect;
return polygon;
},
_getItemWidth: function (seriesIndex, value) {
var serie = this.series[seriesIndex];
var location = this._paramsMap[seriesIndex].location;
var min = serie.min;
var max = serie.max;
var minSize = number.parsePercent(serie.minSize, location.width);
var maxSize = number.parsePercent(serie.maxSize, location.width);
return value * (maxSize - minSize) / (max - min);
},
getPolygon: function (seriesIndex, dataIndex, defaultColor, xLT, y, topWidth, bottomWidth, height, align) {
var serie = this.series[seriesIndex];
var data = serie.data[dataIndex];
var queryTarget = [
data,
serie
];
var normal = this.deepMerge(queryTarget, 'itemStyle.normal') || {};
var emphasis = this.deepMerge(queryTarget, 'itemStyle.emphasis') || {};
var normalColor = this.getItemStyleColor(normal.color, seriesIndex, dataIndex, data) || defaultColor;
var emphasisColor = this.getItemStyleColor(emphasis.color, seriesIndex, dataIndex, data) || (typeof normalColor === 'string' ? zrColor.lift(normalColor, -0.2) : normalColor);
var xLB;
switch (align) {
case 'left':
xLB = xLT;
break;
case 'right':
xLB = xLT + (topWidth - bottomWidth);
break;
default:
xLB = xLT + (topWidth - bottomWidth) / 2;
break;
}
var polygon = {
zlevel: this.getZlevelBase(),
z: this.getZBase(),
clickable: this.deepQuery(queryTarget, 'clickable'),
style: {
pointList: [
[
xLT,
y
],
[
xLT + topWidth,
y
],
[
xLB + bottomWidth,
y + height
],
[
xLB,
y + height
]
],
brushType: 'both',
color: normalColor,
lineWidth: normal.borderWidth,
strokeColor: normal.borderColor
},
highlightStyle: {
color: emphasisColor,
lineWidth: emphasis.borderWidth,
strokeColor: emphasis.borderColor
}
};
if (this.deepQuery([
data,
serie,
this.option
], 'calculable')) {
this.setCalculable(polygon);
polygon.draggable = true;
}
return new PolygonShape(polygon);
},
getLabel: function (seriesIndex, dataIndex, defaultColor, x, y, topWidth, bottomWidth, height, align) {
var serie = this.series[seriesIndex];
var data = serie.data[dataIndex];
var location = this._paramsMap[seriesIndex].location;
var itemStyle = zrUtil.merge(zrUtil.clone(data.itemStyle) || {}, serie.itemStyle);
var status = 'normal';
var labelControl = itemStyle[status].label;
var textStyle = labelControl.textStyle || {};
var lineLength = itemStyle[status].labelLine.length;
var text = this.getLabelText(seriesIndex, dataIndex, status);
var textFont = this.getFont(textStyle);
var textAlign;
var textColor = defaultColor;
labelControl.position = labelControl.position || itemStyle.normal.label.position;
if (labelControl.position === 'inner' || labelControl.position === 'inside' || labelControl.position === 'center') {
textAlign = align;
textColor = Math.max(topWidth, bottomWidth) / 2 > zrArea.getTextWidth(text, textFont) ? '#fff' : zrColor.reverse(defaultColor);
} else if (labelControl.position === 'left') {
textAlign = 'right';
} else {
textAlign = 'left';
}
var textShape = {
zlevel: this.getZlevelBase(),
z: this.getZBase() + 1,
style: {
x: this._getLabelPoint(labelControl.position, x, location, topWidth, bottomWidth, lineLength, align),
y: y + height / 2,
color: textStyle.color || textColor,
text: text,
textAlign: textStyle.align || textAlign,
textBaseline: textStyle.baseline || 'middle',
textFont: textFont
}
};
status = 'emphasis';
labelControl = itemStyle[status].label || labelControl;
textStyle = labelControl.textStyle || textStyle;
lineLength = itemStyle[status].labelLine.length || lineLength;
labelControl.position = labelControl.position || itemStyle.normal.label.position;
text = this.getLabelText(seriesIndex, dataIndex, status);
textFont = this.getFont(textStyle);
textColor = defaultColor;
if (labelControl.position === 'inner' || labelControl.position === 'inside' || labelControl.position === 'center') {
textAlign = align;
textColor = Math.max(topWidth, bottomWidth) / 2 > zrArea.getTextWidth(text, textFont) ? '#fff' : zrColor.reverse(defaultColor);
} else if (labelControl.position === 'left') {
textAlign = 'right';
} else {
textAlign = 'left';
}
textShape.highlightStyle = {
x: this._getLabelPoint(labelControl.position, x, location, topWidth, bottomWidth, lineLength, align),
color: textStyle.color || textColor,
text: text,
textAlign: textStyle.align || textAlign,
textFont: textFont,
brushType: 'fill'
};
return new TextShape(textShape);
},
getLabelText: function (seriesIndex, dataIndex, status) {
var series = this.series;
var serie = series[seriesIndex];
var data = serie.data[dataIndex];
var formatter = this.deepQuery([
data,
serie
], 'itemStyle.' + status + '.label.formatter');
if (formatter) {
if (typeof formatter === 'function') {
return formatter.call(this.myChart, {
seriesIndex: seriesIndex,
seriesName: serie.name || '',
series: serie,
dataIndex: dataIndex,
data: data,
name: data.name,
value: data.value
});
} else if (typeof formatter === 'string') {
formatter = formatter.replace('{a}', '{a0}').replace('{b}', '{b0}').replace('{c}', '{c0}').replace('{a0}', serie.name).replace('{b0}', data.name).replace('{c0}', data.value);
return formatter;
}
} else {
return data.name;
}
},
getLabelLine: function (seriesIndex, dataIndex, defaultColor, x, y, topWidth, bottomWidth, height, align) {
var serie = this.series[seriesIndex];
var data = serie.data[dataIndex];
var location = this._paramsMap[seriesIndex].location;
var itemStyle = zrUtil.merge(zrUtil.clone(data.itemStyle) || {}, serie.itemStyle);
var status = 'normal';
var labelLineControl = itemStyle[status].labelLine;
var lineLength = itemStyle[status].labelLine.length;
var lineStyle = labelLineControl.lineStyle || {};
var labelControl = itemStyle[status].label;
labelControl.position = labelControl.position || itemStyle.normal.label.position;
var lineShape = {
zlevel: this.getZlevelBase(),
z: this.getZBase() + 1,
hoverable: false,
style: {
xStart: this._getLabelLineStartPoint(x, location, topWidth, bottomWidth, align),
yStart: y + height / 2,
xEnd: this._getLabelPoint(labelControl.position, x, location, topWidth, bottomWidth, lineLength, align),
yEnd: y + height / 2,
strokeColor: lineStyle.color || defaultColor,
lineType: lineStyle.type,
lineWidth: lineStyle.width
}
};
status = 'emphasis';
labelLineControl = itemStyle[status].labelLine || labelLineControl;
lineLength = itemStyle[status].labelLine.length || lineLength;
lineStyle = labelLineControl.lineStyle || lineStyle;
labelControl = itemStyle[status].label || labelControl;
labelControl.position = labelControl.position;
lineShape.highlightStyle = {
xEnd: this._getLabelPoint(labelControl.position, x, location, topWidth, bottomWidth, lineLength, align),
strokeColor: lineStyle.color || defaultColor,
lineType: lineStyle.type,
lineWidth: lineStyle.width
};
return new LineShape(lineShape);
},
_getLabelPoint: function (position, x, location, topWidth, bottomWidth, lineLength, align) {
position = position === 'inner' || position === 'inside' ? 'center' : position;
switch (position) {
case 'center':
return align == 'center' ? x + topWidth / 2 : align == 'left' ? x + 10 : x + topWidth - 10;
case 'left':
if (lineLength === 'auto') {
return location.x - 10;
} else {
return align == 'center' ? location.centerX - Math.max(topWidth, bottomWidth) / 2 - lineLength : align == 'right' ? x - (topWidth < bottomWidth ? bottomWidth - topWidth : 0) - lineLength : location.x - lineLength;
}
break;
default:
if (lineLength === 'auto') {
return location.x + location.width + 10;
} else {
return align == 'center' ? location.centerX + Math.max(topWidth, bottomWidth) / 2 + lineLength : align == 'right' ? location.x + location.width + lineLength : x + Math.max(topWidth, bottomWidth) + lineLength;
}
}
},
_getLabelLineStartPoint: function (x, location, topWidth, bottomWidth, align) {
return align == 'center' ? location.centerX : topWidth < bottomWidth ? x + Math.min(topWidth, bottomWidth) / 2 : x + Math.max(topWidth, bottomWidth) / 2;
},
_needLabel: function (serie, data, isEmphasis) {
return this.deepQuery([
data,
serie
], 'itemStyle.' + (isEmphasis ? 'emphasis' : 'normal') + '.label.show');
},
_needLabelLine: function (serie, data, isEmphasis) {
return this.deepQuery([
data,
serie
], 'itemStyle.' + (isEmphasis ? 'emphasis' : 'normal') + '.labelLine.show');
},
refresh: function (newOption) {
if (newOption) {
this.option = newOption;
this.series = newOption.series;
}
this.backupShapeList();
this._buildShape();
}
};
zrUtil.inherits(Funnel, ChartBase);
require('../chart').define('funnel', Funnel);
return Funnel;
});