/** * echarts图表类:漏斗图 * * @desc echarts基于Canvas,纯Javascript图表库,提供直观,生动,可交互,可个性化定制的数据统计图表。 * @author Kener (@Kener-林峰, kener.linfeng@gmail.com) * */ define(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, // width: {totalWidth} - x - x2, // height: {totalHeight} - y - y2, min: 0, max: 100, minSize: '0%', maxSize: '100%', sort: 'descending', // 'ascending', 'descending' gap: 0, funnelAlign: 'center', itemStyle: { normal: { // color: 各异, borderColor: '#fff', borderWidth: 1, label: { show: true, position: 'outer' // formatter: 标签文本格式器,同Tooltip.formatter,不支持异步回调 // textStyle: null // 默认使用全局文本样式,详见TEXTSTYLE }, labelLine: { show: true, length: 10, lineStyle: { // color: 各异, width: 1, type: 'solid' } } }, emphasis: { // color: 各异, 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'); /** * 构造函数 * @param {Object} messageCenter echart消息中心 * @param {ZRender} zr zrender实例 * @param {Object} series 数据 * @param {Object} component 组件 */ 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(); }, /** * 构建单个仪表盘 * * @param {number} seriesIndex 系列索引 */ _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 // color ? 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 - min) * (maxSize - minSize) / (max - min) + minSize; }, /** * 构建扇形 */ 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: serie.zlevel, z: serie.z, 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); }, /** * 需要显示则会有返回构建好的shape,否则返回undefined */ 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; // serie里有默认配置,放心大胆的用! var itemStyle = zrUtil.merge( zrUtil.clone(data.itemStyle) || {}, serie.itemStyle ); var status = 'normal'; // label配置 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 { // 右侧显示,默认 labelControl.position === 'outer' || 'right) textAlign = 'left'; } var textShape = { zlevel: serie.zlevel, z: serie.z + 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'; // label配置 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 { // 右侧显示,默认 labelControl.position === 'outer' || 'right) 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); }, /** * 根据lable.format计算label text */ 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; } }, /** * 需要显示则会有返回构建好的shape,否则返回undefined */ 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; // serie里有默认配置,放心大胆的用! var itemStyle = zrUtil.merge( zrUtil.clone(data.itemStyle) || {}, serie.itemStyle ); var status = 'normal'; // labelLine配置 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: serie.zlevel, z: serie.z + 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'; // labelLine配置 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); }, /** * 返回特定状态(normal or emphasis)下是否需要显示label标签文本 * @param {Object} serie * @param {Object} data * @param {boolean} isEmphasis true is 'emphasis' and false is 'normal' */ _needLabel: function (serie, data, isEmphasis) { return this.deepQuery( [data, serie], 'itemStyle.' + (isEmphasis ? 'emphasis' : 'normal') + '.label.show' ); }, /** * 返回特定状态(normal or emphasis)下是否需要显示labelLine标签视觉引导线 * @param {Object} serie * @param {Object} data * @param {boolean} isEmphasis true is 'emphasis' and false is 'normal' */ _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; });