/** * echarts组件: 数值轴 * * @desc echarts基于Canvas,纯Javascript图表库,提供直观,生动,可交互,可个性化定制的数据统计图表。 * @author Kener (@Kener-林峰, kener.linfeng@gmail.com) * */ define(function (require) { var Base = require('./base'); // 图形依赖 var TextShape = require('zrender/shape/Text'); var LineShape = require('zrender/shape/Line'); var RectangleShape = require('zrender/shape/Rectangle'); var ecConfig = require('../config'); // 数值型坐标轴默认参数 ecConfig.valueAxis = { zlevel: 0, // 一级层叠 z: 0, // 二级层叠 show: true, position: 'left', // 位置 name: '', // 坐标轴名字,默认为空 nameLocation: 'end', // 坐标轴名字位置,支持'start' | 'end' nameTextStyle: {}, // 坐标轴文字样式,默认取全局样式 boundaryGap: [0, 0], // 数值起始和结束两端空白策略 // min: null, // 最小值 // max: null, // 最大值 // scale: false, // 脱离0值比例,放大聚焦到最终_min,_max区间 // splitNumber: 5, // 分割段数,默认为5 axisLine: { // 坐标轴线 show: true, // 默认显示,属性show控制显示与否 onZero: true, lineStyle: { // 属性lineStyle控制线条样式 color: '#48b', width: 2, type: 'solid' } }, axisTick: { // 坐标轴小标记 show: false, // 属性show控制显示与否,默认不显示 inside: false, // 控制小标记是否在grid里 length :5, // 属性length控制线长 lineStyle: { // 属性lineStyle控制线条样式 color: '#333', width: 1 } }, axisLabel: { // 坐标轴文本标签,详见axis.axisLabel show: true, rotate: 0, margin: 8, // clickable: false, // formatter: null, textStyle: { // 其余属性默认使用全局文本样式,详见TEXTSTYLE color: '#333' } }, splitLine: { // 分隔线 show: true, // 默认显示,属性show控制显示与否 lineStyle: { // 属性lineStyle(详见lineStyle)控制线条样式 color: ['#ccc'], width: 1, type: 'solid' } }, splitArea: { // 分隔区域 show: false, // 默认不显示,属性show控制显示与否 areaStyle: { // 属性areaStyle(详见areaStyle)控制区域样式 color: ['rgba(250,250,250,0.3)','rgba(200,200,200,0.3)'] } } }; var ecDate = require('../util/date'); var zrUtil = require('zrender/tool/util'); /** * 构造函数 * @param {Object} messageCenter echart消息中心 * @param {ZRender} zr zrender实例 * @param {Object} option 类目轴参数 * @param {Object} component 组件 * @param {Array} series 数据对象 */ function ValueAxis(ecTheme, messageCenter, zr, option, myChart, axisBase, series) { if (!series || series.length === 0) { console.err('option.series.length == 0.'); return; } Base.call(this, ecTheme, messageCenter, zr, option, myChart); this.series = series; this.grid = this.component.grid; for (var method in axisBase) { this[method] = axisBase[method]; } this.refresh(option, series); } ValueAxis.prototype = { type: ecConfig.COMPONENT_TYPE_AXIS_VALUE, _buildShape: function () { this._hasData = false; this._calculateValue(); if (!this._hasData || !this.option.show) { return; } this.option.splitArea.show && this._buildSplitArea(); this.option.splitLine.show && this._buildSplitLine(); this.option.axisLine.show && this._buildAxisLine(); this.option.axisTick.show && this._buildAxisTick(); this.option.axisLabel.show && this._buildAxisLabel(); for (var i = 0, l = this.shapeList.length; i < l; i++) { this.zr.addShape(this.shapeList[i]); } }, // 小标记 _buildAxisTick: function () { var axShape; var data = this._valueList; var dataLength = this._valueList.length; var tickOption = this.option.axisTick; var length = tickOption.length; var color = tickOption.lineStyle.color; var lineWidth = tickOption.lineStyle.width; if (this.isHorizontal()) { // 横向 var yPosition = this.option.position === 'bottom' ? (tickOption.inside ? (this.grid.getYend() - length - 1) : (this.grid.getYend()) + 1) : (tickOption.inside ? (this.grid.getY() + 1) : (this.grid.getY() - length - 1)); var x; for (var i = 0; i < dataLength; i++) { // 亚像素优化 x = this.subPixelOptimize(this.getCoord(data[i]), lineWidth); axShape = { _axisShape: 'axisTick', zlevel: this.getZlevelBase(), z: this.getZBase(), hoverable: false, style: { xStart: x, yStart: yPosition, xEnd: x, yEnd: yPosition + length, strokeColor: color, lineWidth: lineWidth } }; this.shapeList.push(new LineShape(axShape)); } } else { // 纵向 var xPosition = this.option.position === 'left' ? (tickOption.inside ? (this.grid.getX() + 1) : (this.grid.getX() - length - 1)) : (tickOption.inside ? (this.grid.getXend() - length - 1) : (this.grid.getXend() + 1)); var y; for (var i = 0; i < dataLength; i++) { // 亚像素优化 y = this.subPixelOptimize(this.getCoord(data[i]), lineWidth); axShape = { _axisShape: 'axisTick', zlevel: this.getZlevelBase(), z: this.getZBase(), hoverable: false, style: { xStart: xPosition, yStart: y, xEnd: xPosition + length, yEnd: y, strokeColor: color, lineWidth: lineWidth } }; this.shapeList.push(new LineShape(axShape)); } } }, // 坐标轴文本 _buildAxisLabel: function () { var axShape; var data = this._valueList; var dataLength = this._valueList.length; var rotate = this.option.axisLabel.rotate; var margin = this.option.axisLabel.margin; var clickable = this.option.axisLabel.clickable; var textStyle = this.option.axisLabel.textStyle; if (this.isHorizontal()) { // 横向 var yPosition; var baseLine; if (this.option.position === 'bottom') { yPosition = this.grid.getYend() + margin; baseLine = 'top'; } else { yPosition = this.grid.getY() - margin; baseLine = 'bottom'; } for (var i = 0; i < dataLength; i++) { axShape = { zlevel: this.getZlevelBase(), z: this.getZBase() +3, hoverable: false, style: { x: this.getCoord(data[i]), y: yPosition, color: typeof textStyle.color === 'function' ? textStyle.color(data[i]) : textStyle.color, text: this._valueLabel[i], textFont: this.getFont(textStyle), textAlign: textStyle.align || 'center', textBaseline: textStyle.baseline || baseLine } }; if (rotate) { axShape.style.textAlign = rotate > 0 ? (this.option.position === 'bottom' ? 'right' : 'left') : (this.option.position === 'bottom' ? 'left' : 'right'); axShape.rotation = [ rotate * Math.PI / 180, axShape.style.x, axShape.style.y ]; } this.shapeList.push(new TextShape( this._axisLabelClickable(clickable, axShape) )); } } else { // 纵向 var xPosition; var align; if (this.option.position === 'left') { xPosition = this.grid.getX() - margin; align = 'right'; } else { xPosition = this.grid.getXend() + margin; align = 'left'; } for (var i = 0; i < dataLength; i++) { axShape = { zlevel: this.getZlevelBase(), z: this.getZBase() + 3, hoverable: false, style: { x: xPosition, y: this.getCoord(data[i]), color: typeof textStyle.color === 'function' ? textStyle.color(data[i]) : textStyle.color, text: this._valueLabel[i], textFont: this.getFont(textStyle), textAlign: textStyle.align || align, textBaseline: textStyle.baseline || ( (i === 0 && this.option.name !== '') ? 'bottom' : (i === dataLength - 1 && this.option.name !== '') ? 'top' : 'middle' ) } }; if (rotate) { axShape.rotation = [ rotate * Math.PI / 180, axShape.style.x, axShape.style.y ]; } this.shapeList.push(new TextShape( this._axisLabelClickable(clickable, axShape) )); } } }, _buildSplitLine: function () { var axShape; var data = this._valueList; var dataLength = this._valueList.length; var sLineOption = this.option.splitLine; var lineType = sLineOption.lineStyle.type; var lineWidth = sLineOption.lineStyle.width; var color = sLineOption.lineStyle.color; color = color instanceof Array ? color : [color]; var colorLength = color.length; if (this.isHorizontal()) { // 横向 var sy = this.grid.getY(); var ey = this.grid.getYend(); var x; for (var i = 0; i < dataLength; i++) { // 亚像素优化 x = this.subPixelOptimize(this.getCoord(data[i]), lineWidth); axShape = { zlevel: this.getZlevelBase(), z: this.getZBase(), hoverable: false, style: { xStart: x, yStart: sy, xEnd: x, yEnd: ey, strokeColor: color[i % colorLength], lineType: lineType, lineWidth: lineWidth } }; this.shapeList.push(new LineShape(axShape)); } } else { // 纵向 var sx = this.grid.getX(); var ex = this.grid.getXend(); var y; for (var i = 0; i < dataLength; i++) { // 亚像素优化 y = this.subPixelOptimize(this.getCoord(data[i]), lineWidth); axShape = { zlevel: this.getZlevelBase(), z: this.getZBase(), hoverable: false, style: { xStart: sx, yStart: y, xEnd: ex, yEnd: y, strokeColor: color[i % colorLength], lineType: lineType, lineWidth: lineWidth } }; this.shapeList.push(new LineShape(axShape)); } } }, _buildSplitArea: function () { var axShape; var color = this.option.splitArea.areaStyle.color; if (!(color instanceof Array)) { // 非数组一律认为是单一颜色的字符串,单一颜色则用一个背景,颜色错误不负责啊!!! axShape = { zlevel: this.getZlevelBase(), z: this.getZBase(), hoverable: false, style: { x: this.grid.getX(), y: this.grid.getY(), width: this.grid.getWidth(), height: this.grid.getHeight(), color: color // type: this.option.splitArea.areaStyle.type, } }; this.shapeList.push(new RectangleShape(axShape)); } else { // 多颜色 var colorLength = color.length; var data = this._valueList; var dataLength = this._valueList.length; if (this.isHorizontal()) { // 横向 var y = this.grid.getY(); var height = this.grid.getHeight(); var lastX = this.grid.getX(); var curX; for (var i = 0; i <= dataLength; i++) { curX = i < dataLength ? this.getCoord(data[i]) : this.grid.getXend(); axShape = { zlevel: this.getZlevelBase(), z: this.getZBase(), hoverable: false, style: { x: lastX, y: y, width: curX - lastX, height: height, color: color[i % colorLength] // type: this.option.splitArea.areaStyle.type, } }; this.shapeList.push(new RectangleShape(axShape)); lastX = curX; } } else { // 纵向 var x = this.grid.getX(); var width = this.grid.getWidth(); var lastYend = this.grid.getYend(); var curY; for (var i = 0; i <= dataLength; i++) { curY = i < dataLength ? this.getCoord(data[i]) : this.grid.getY(); axShape = { zlevel: this.getZlevelBase(), z: this.getZBase(), hoverable: false, style: { x: x, y: curY, width: width, height: lastYend - curY, color: color[i % colorLength] // type: this.option.splitArea.areaStyle.type } }; this.shapeList.push(new RectangleShape(axShape)); lastYend = curY; } } } }, /** * 极值计算 */ _calculateValue: function () { if (isNaN(this.option.min - 0) || isNaN(this.option.max - 0)) { // 有一个没指定都得算 // 数据整形 var data = {}; // 整形后数据抽取 var xIdx; var yIdx; var legend = this.component.legend; for (var i = 0, l = this.series.length; i < l; i++) { if (this.series[i].type != ecConfig.CHART_TYPE_LINE && this.series[i].type != ecConfig.CHART_TYPE_BAR && this.series[i].type != ecConfig.CHART_TYPE_SCATTER && this.series[i].type != ecConfig.CHART_TYPE_K && this.series[i].type != ecConfig.CHART_TYPE_EVENTRIVER ) { // 非坐标轴支持的不算极值 continue; } // 请允许我写开,跟上面一个不是一样东西 if (legend && !legend.isSelected(this.series[i].name)){ continue; } // 不指定默认为第一轴线 xIdx = this.series[i].xAxisIndex || 0; yIdx = this.series[i].yAxisIndex || 0; if ((this.option.xAxisIndex != xIdx) && (this.option.yAxisIndex != yIdx) ) { // 不是自己的数据不计算极值 continue; } this._calculSum(data, i); } // 找极值 var oriData; // 原始数据 for (var i in data){ oriData = data[i]; for (var j = 0, k = oriData.length; j < k; j++) { if (!isNaN(oriData[j])){ this._hasData = true; this._min = oriData[j]; this._max = oriData[j]; break; } } if (this._hasData) { break; } } for (var i in data){ oriData = data[i]; for (var j = 0, k = oriData.length; j < k; j++) { if (!isNaN(oriData[j])){ this._min = Math.min(this._min, oriData[j]); this._max = Math.max(this._max, oriData[j]); } } } // console.log(this._min,this._max,'vvvvv111111',this.option.type) // log情况暂时禁用boundaryGap。 var boundaryGap = this.option.type !== 'log' ? this.option.boundaryGap : [0, 0]; var gap = Math.abs(this._max - this._min); this._min = isNaN(this.option.min - 0) ? (this._min - Math.abs(gap * boundaryGap[0])) : (this.option.min - 0); // 指定min忽略boundaryGay[0] this._max = isNaN(this.option.max - 0) ? (this._max + Math.abs(gap * boundaryGap[1])) : (this.option.max - 0); // 指定max忽略boundaryGay[1] if (this._min === this._max) { if (this._max === 0) { // 修复全0数据 this._max = 1; } // 修复最大值==最小值时数据整形 else if (this._max > 0) { this._min = this._max / this.option.splitNumber != null ? this.option.splitNumber : 5; } else { // this._max < 0 this._max = this._max / this.option.splitNumber != null ? this.option.splitNumber : 5; } } if (this.option.type === 'time') { this._reformTimeValue(); } else if (this.option.type === 'log') { this._reformLogValue(); } else { this._reformValue(this.option.scale); } } else { this._hasData = true; // 用户指定min max就不多管闲事了 this._min = this.option.min - 0; // 指定min忽略boundaryGay[0] this._max = this.option.max - 0; // 指定max忽略boundaryGay[1] if (this.option.type === 'time') { this._reformTimeValue(); } else if (this.option.type === 'log') { this._reformLogValue(); } else { this._customerValue(); } } }, /** * 内部使用,计算某系列下的堆叠和 */ _calculSum: function (data, i) { var key = this.series[i].name || 'kener'; var value; var oriData; if (!this.series[i].stack) { data[key] = data[key] || []; if (this.series[i].type != ecConfig.CHART_TYPE_EVENTRIVER) { oriData = this.series[i].data; for (var j = 0, k = oriData.length; j < k; j++) { value = this.getDataFromOption(oriData[j]); if (this.series[i].type === ecConfig.CHART_TYPE_K) { data[key].push(value[0]); data[key].push(value[1]); data[key].push(value[2]); data[key].push(value[3]); } else if (value instanceof Array) { // scatter 、 不等距 line bar if (this.option.xAxisIndex != -1) { data[key].push( this.option.type != 'time' ? value[0] : ecDate.getNewDate(value[0]) ); } if (this.option.yAxisIndex != -1) { data[key].push( this.option.type != 'time' ? value[1] : ecDate.getNewDate(value[1]) ); } } else { data[key].push(value); } } } else { // eventRiver oriData = this.series[i].data; for (var j = 0, k = oriData.length; j < k; j++) { var evolution = oriData[j].evolution; for (var m = 0, n = evolution.length; m < n; m++) { data[key].push(ecDate.getNewDate(evolution[m].time)); } } } } else { // 堆积数据,需要区分正负向堆积 var keyP = '__Magic_Key_Positive__' + this.series[i].stack; var keyN = '__Magic_Key_Negative__' + this.series[i].stack; data[keyP] = data[keyP] || []; data[keyN] = data[keyN] || []; data[key] = data[key] || []; // scale下还需要记录每一个量 oriData = this.series[i].data; for (var j = 0, k = oriData.length; j < k; j++) { value = this.getDataFromOption(oriData[j]); if (value === '-') { continue; } value = value - 0; if (value >= 0) { if (data[keyP][j] != null) { data[keyP][j] += value; } else { data[keyP][j] = value; } } else { if (data[keyN][j] != null) { data[keyN][j] += value; } else { data[keyN][j] = value; } } if (this.option.scale) { data[key].push(value); } } } }, /** * 找到原始数据的极值后根据选项整形最终 this._min / this._max / this._valueList * 如果你不知道这个“整形”的用义,请不要试图去理解和修改这个方法!找我也没用,我相信我已经记不起来! * 如果你有更简洁的数学推导欢迎重写,后果自负~ * * by kener.linfeng@gmail.com 2013-1-8 * -------- * 感谢谢世威(https://github.com/i6ma),终于有人改这个方法了 * by Kener 2014-11-6 */ _reformValue: function (scale) { var smartSteps = require('../util/smartSteps'); var splitNumber = this.option.splitNumber; // 非scale下双正,修正最小值为0 if (!scale && this._min >= 0 && this._max >= 0) { this._min = 0; } // 非scale下双负,修正最大值为0 if (!scale && this._min <= 0 && this._max <= 0) { this._max = 0; } var stepOpt = smartSteps(this._min, this._max, splitNumber); splitNumber = splitNumber != null ? splitNumber : stepOpt.secs; //this.option.splitNumber = splitNumber; this._min = stepOpt.min; this._max = stepOpt.max; this._valueList = stepOpt.pnts; this._reformLabelData(); }, /** * 格式化时间值 */ _reformTimeValue : function() { var splitNumber = this.option.splitNumber != null ? this.option.splitNumber : 5; // 最优解 var curValue = ecDate.getAutoFormatter(this._min, this._max, splitNumber); // 目标 var formatter = curValue.formatter; var gapValue = curValue.gapValue; this._valueList = [ecDate.getNewDate(this._min)]; var startGap; switch (formatter) { case 'week' : startGap = ecDate.nextMonday(this._min); break; case 'month' : startGap = ecDate.nextNthOnMonth(this._min, 1); break; case 'quarter' : startGap = ecDate.nextNthOnQuarterYear(this._min, 1); break; case 'half-year' : startGap = ecDate.nextNthOnHalfYear(this._min, 1); break; case 'year' : startGap = ecDate.nextNthOnYear(this._min, 1); break; default : // 大于2小时需要考虑时区不能直接取整 if (gapValue <= 3600000 * 2) { startGap = (Math.floor(this._min / gapValue) + 1) * gapValue; } else { startGap = ecDate.getNewDate(this._min - (-gapValue)); startGap.setHours(Math.round(startGap.getHours() / 6) * 6); startGap.setMinutes(0); startGap.setSeconds(0); } break; } if (startGap - this._min < gapValue / 2) { startGap -= -gapValue; } // console.log(startGap,gapValue,this._min, this._max,formatter) curValue = ecDate.getNewDate(startGap); splitNumber *= 1.5; while (splitNumber-- >= 0) { if (formatter == 'month' || formatter == 'quarter' || formatter == 'half-year' || formatter == 'year' ) { curValue.setDate(1); } if (this._max - curValue < gapValue / 2) { break; } this._valueList.push(curValue); curValue = ecDate.getNewDate(curValue - (-gapValue)); } this._valueList.push(ecDate.getNewDate(this._max)); this._reformLabelData((function (formatterStr) { return function (value) { return ecDate.format(formatterStr, value); }; })(formatter)); }, _customerValue: function () { var accMath = require('../util/accMath'); var splitNumber = this.option.splitNumber != null ? this.option.splitNumber : 5; var splitGap = (this._max - this._min) / splitNumber; this._valueList = []; for (var i = 0; i <= splitNumber; i++) { this._valueList.push(accMath.accAdd(this._min, accMath.accMul(splitGap, i))); } this._reformLabelData(); }, _reformLogValue: function() { // log数轴本质就是缩放,相当于默认this.option.scale === true,所以不修正_min和_max到0。 var thisOption = this.option; var result = require('../util/smartLogSteps')({ dataMin: this._min, dataMax: this._max, logPositive: thisOption.logPositive, logLabelBase: thisOption.logLabelBase, splitNumber: thisOption.splitNumber }); this._min = result.dataMin; this._max = result.dataMax; this._valueList = result.tickList; // {value2Coord: {Function}, coord2Value: {Function}} this._dataMappingMethods = result.dataMappingMethods; this._reformLabelData(result.labelFormatter); }, _reformLabelData: function (innerFormatter) { this._valueLabel = []; var formatter = this.option.axisLabel.formatter; if (formatter) { for (var i = 0, l = this._valueList.length; i < l; i++) { if (typeof formatter === 'function') { this._valueLabel.push( innerFormatter ? formatter.call(this.myChart, this._valueList[i], innerFormatter) : formatter.call(this.myChart, this._valueList[i]) ); } else if (typeof formatter === 'string') { this._valueLabel.push( innerFormatter ? ecDate.format(formatter, this._valueList[i]) : formatter.replace('{value}',this._valueList[i]) ); } } } else { for (var i = 0, l = this._valueList.length; i < l; i++) { this._valueLabel.push( innerFormatter ? innerFormatter(this._valueList[i]) : this.numAddCommas(this._valueList[i]) // 每三位默认加,格式化 ); } } }, getExtremum: function () { this._calculateValue(); var dataMappingMethods = this._dataMappingMethods; return { min: this._min, max: this._max, dataMappingMethods: dataMappingMethods ? zrUtil.merge({}, dataMappingMethods) : null }; }, /** * 刷新 */ refresh: function (newOption, newSeries) { if (newOption) { this.option = this.reformOption(newOption); // 通用字体设置 this.option.axisLabel.textStyle = zrUtil.merge( this.option.axisLabel.textStyle || {}, this.ecTheme.textStyle ); this.series = newSeries; } if (this.zr) { // 数值轴的另外一个功能只是用来计算极值 this.clear(); this._buildShape(); } }, // 根据值换算位置 getCoord: function (value) { if (this._dataMappingMethods) { value = this._dataMappingMethods.value2Coord(value); } value = value < this._min ? this._min : value; value = value > this._max ? this._max : value; var result; if (!this.isHorizontal()) { // 纵向 result = this.grid.getYend() - (value - this._min) / (this._max - this._min) * this.grid.getHeight(); } else { // 横向 result = this.grid.getX() + (value - this._min) / (this._max - this._min) * this.grid.getWidth(); } return result; // Math.floor可能引起一些偏差,但性能会更好 /* 准确更重要 return (value === this._min || value === this._max) ? result : Math.floor(result); */ }, // 根据值换算绝对大小 getCoordSize: function (value) { if (!this.isHorizontal()) { // 纵向 return Math.abs(value / (this._max - this._min) * this.grid.getHeight()); } else { // 横向 return Math.abs(value / (this._max - this._min) * this.grid.getWidth()); } }, // 根据位置换算值 getValueFromCoord: function(coord) { var result; if (!this.isHorizontal()) { // 纵向 coord = coord < this.grid.getY() ? this.grid.getY() : coord; coord = coord > this.grid.getYend() ? this.grid.getYend() : coord; result = this._max - (coord - this.grid.getY()) / this.grid.getHeight() * (this._max - this._min); } else { // 横向 coord = coord < this.grid.getX() ? this.grid.getX() : coord; coord = coord > this.grid.getXend() ? this.grid.getXend() : coord; result = this._min + (coord - this.grid.getX()) / this.grid.getWidth() * (this._max - this._min); } if (this._dataMappingMethods) { result = this._dataMappingMethods.coord2Value(result); } return result.toFixed(2) - 0; }, isMaindAxis : function (value) { for (var i = 0, l = this._valueList.length; i < l; i++) { if (this._valueList[i] === value) { return true; } } return false; } }; zrUtil.inherits(ValueAxis, Base); require('../component').define('valueAxis', ValueAxis); return ValueAxis; });