/** * echarts组件:时间轴组件 * * @desc echarts基于Canvas,纯Javascript图表库,提供直观,生动,可交互,可个性化定制的数据统计图表。 * @author Kener (@Kener-林峰, kener.linfeng@gmail.com) * */ define(function (require) { var Base = require('./base'); // 图形依赖 var RectangleShape = require('zrender/shape/Rectangle'); var IconShape = require('../util/shape/Icon'); var ChainShape = require('../util/shape/Chain'); var ecConfig = require('../config'); ecConfig.timeline = { zlevel: 0, // 一级层叠 z: 4, // 二级层叠 show: true, type: 'time', // 模式是时间类型,支持 number notMerge: false, realtime: true, x: 80, // y: {number}, x2: 80, y2: 0, // width: {totalWidth} - x - x2, height: 50, backgroundColor: 'rgba(0,0,0,0)', // 时间轴背景颜色 borderColor: '#ccc', // 时间轴边框颜色 borderWidth: 0, // 时间轴边框线宽,单位px,默认为0(无边框) padding: 5, // 时间轴内边距,单位px,默认各方向内边距为5, controlPosition: 'left', // 'right' | 'none' autoPlay: false, loop: true, playInterval: 2000, // 播放时间间隔,单位ms lineStyle: { width: 1, color: '#666', type: 'dashed' }, label: { // 文本标签 show: true, interval: 'auto', rotate: 0, // formatter: null, textStyle: { // 其余属性默认使用全局文本样式,详见TEXTSTYLE color: '#333' } }, checkpointStyle: { symbol: 'auto', symbolSize: 'auto', color: 'auto', borderColor: 'auto', borderWidth: 'auto', label: { // 文本标签 show: false, textStyle: { // 其余属性默认使用全局文本样式,详见TEXTSTYLE color: 'auto' } } }, controlStyle: { itemSize: 15, itemGap: 5, normal: { color: '#333'}, emphasis: { color: '#1e90ff'} }, symbol: 'emptyDiamond', symbolSize: 4, currentIndex: 0 // data: [] }; var zrUtil = require('zrender/tool/util'); var zrArea = require('zrender/tool/area'); var zrEvent = require('zrender/tool/event'); /** * 构造函数 * @param {Object} messageCenter echart消息中心 * @param {ZRender} zr zrender实例 * @param {Object} option 图表参数 */ function Timeline(ecTheme, messageCenter, zr, option, myChart) { Base.call(this, ecTheme, messageCenter, zr, option, myChart); var self = this; self._onclick = function(param) { return self.__onclick(param); }; self._ondrift = function (dx, dy) { return self.__ondrift(this, dx, dy); }; self._ondragend = function () { return self.__ondragend(); }; self._setCurrentOption = function() { var timelineOption = self.timelineOption; self.currentIndex %= timelineOption.data.length; // console.log(self.currentIndex); var curOption = self.options[self.currentIndex] || {}; self.myChart.setOption(curOption, timelineOption.notMerge); self.messageCenter.dispatch( ecConfig.EVENT.TIMELINE_CHANGED, null, { currentIndex: self.currentIndex, data: timelineOption.data[self.currentIndex].name != null ? timelineOption.data[self.currentIndex].name : timelineOption.data[self.currentIndex] }, self.myChart ); }; self._onFrame = function() { self._setCurrentOption(); self._syncHandleShape(); if (self.timelineOption.autoPlay) { self.playTicket = setTimeout( function() { self.currentIndex += 1; if (!self.timelineOption.loop && self.currentIndex >= self.timelineOption.data.length ) { self.currentIndex = self.timelineOption.data.length - 1; self.stop(); return; } self._onFrame(); }, self.timelineOption.playInterval ); } }; this.setTheme(false); this.options = this.option.options; this.currentIndex = this.timelineOption.currentIndex % this.timelineOption.data.length; if (!this.timelineOption.notMerge && this.currentIndex !== 0) { /* for (var i = 1, l = this.timelineOption.data.length; i < l; i++) { this.options[i] = zrUtil.merge( this.options[i], this.options[i - 1] ); } */ this.options[this.currentIndex] = zrUtil.merge( this.options[this.currentIndex], this.options[0] ); } if (this.timelineOption.show) { this._buildShape(); this._syncHandleShape(); } this._setCurrentOption(); if (this.timelineOption.autoPlay) { var self = this; this.playTicket = setTimeout( function() { self.play(); }, this.ecTheme.animationDuration != null ? this.ecTheme.animationDuration : ecConfig.animationDuration ); } } Timeline.prototype = { type: ecConfig.COMPONENT_TYPE_TIMELINE, _buildShape: function () { // 位置参数,通过计算所得x, y, width, height this._location = this._getLocation(); this._buildBackground(); this._buildControl(); this._chainPoint = this._getChainPoint(); if (this.timelineOption.label.show) { // 标签显示的挑选间隔 var interval = this._getInterval(); for (var i = 0, len = this._chainPoint.length; i < len; i += interval) { this._chainPoint[i].showLabel = true; } } this._buildChain(); this._buildHandle(); for (var i = 0, l = this.shapeList.length; i < l; i++) { this.zr.addShape(this.shapeList[i]); } }, /** * 根据选项计算实体的位置坐标 */ _getLocation: function () { var timelineOption = this.timelineOption; var padding = this.reformCssArray(this.timelineOption.padding); // 水平布局 var zrWidth = this.zr.getWidth(); var x = this.parsePercent(timelineOption.x, zrWidth); var x2 = this.parsePercent(timelineOption.x2, zrWidth); var width; if (timelineOption.width == null) { width = zrWidth - x - x2; x2 = zrWidth - x2; } else { width = this.parsePercent(timelineOption.width, zrWidth); x2 = x + width; } var zrHeight = this.zr.getHeight(); var height = this.parsePercent(timelineOption.height, zrHeight); var y; var y2; if (timelineOption.y != null) { y = this.parsePercent(timelineOption.y, zrHeight); y2 = y + height; } else { y2 = zrHeight - this.parsePercent(timelineOption.y2, zrHeight); y = y2 - height; } return { x: x + padding[3], y: y + padding[0], x2: x2 - padding[1], y2: y2 - padding[2], width: width - padding[1] - padding[3], height: height - padding[0] - padding[2] }; }, _getReformedLabel: function (idx) { var timelineOption = this.timelineOption; var data = timelineOption.data[idx].name != null ? timelineOption.data[idx].name : timelineOption.data[idx]; var formatter = timelineOption.data[idx].formatter || timelineOption.label.formatter; if (formatter) { if (typeof formatter === 'function') { data = formatter.call(this.myChart, data); } else if (typeof formatter === 'string') { data = formatter.replace('{value}', data); } } return data; }, /** * 计算标签显示挑选间隔 */ _getInterval: function () { var chainPoint = this._chainPoint; var timelineOption = this.timelineOption; var interval = timelineOption.label.interval; if (interval === 'auto') { // 麻烦的自适应计算 var fontSize = timelineOption.label.textStyle.fontSize; var data = timelineOption.data; var dataLength = timelineOption.data.length; // 横向 if (dataLength > 3) { var isEnough = false; var labelSpace; var labelSize; interval = 0; while (!isEnough && interval < dataLength) { interval++; isEnough = true; for (var i = interval; i < dataLength; i += interval) { labelSpace = chainPoint[i].x - chainPoint[i - interval].x; if (timelineOption.label.rotate !== 0) { // 有旋转 labelSize = fontSize; } else if (data[i].textStyle) { labelSize = zrArea.getTextWidth( chainPoint[i].name, chainPoint[i].textFont ); } else { // 不定义data级特殊文本样式,用fontSize优化getTextWidth var label = chainPoint[i].name + ''; var wLen = (label.match(/\w/g) || '').length; var oLen = label.length - wLen; labelSize = wLen * fontSize * 2 / 3 + oLen * fontSize; } if (labelSpace < labelSize) { // 放不下,中断循环让interval++ isEnough = false; break; } } } } else { // 少于3个则全部显示 interval = 1; } } else { // 用户自定义间隔 interval = interval - 0 + 1; } return interval; }, /** * 根据选项计算时间链条上的坐标及symbolList */ _getChainPoint: function() { var timelineOption = this.timelineOption; var symbol = timelineOption.symbol.toLowerCase(); var symbolSize = timelineOption.symbolSize; var rotate = timelineOption.label.rotate; var textStyle = timelineOption.label.textStyle; var textFont = this.getFont(textStyle); var dataTextStyle; var data = timelineOption.data; var x = this._location.x; var y = this._location.y + this._location.height / 4 * 3; var width = this._location.x2 - this._location.x; var len = data.length; function _getName(i) { return (data[i].name != null ? data[i].name : data[i] + ''); } var xList = []; if (len > 1) { var boundaryGap = width / len; boundaryGap = boundaryGap > 50 ? 50 : (boundaryGap < 20 ? 5 : boundaryGap); width -= boundaryGap * 2; if (timelineOption.type === 'number') { // 平均分布 for (var i = 0; i < len; i++) { xList.push(x + boundaryGap + width / (len - 1) * i); } } else { // 时间比例 xList[0] = new Date(_getName(0).replace(/-/g, '/')); xList[len - 1] = new Date(_getName(len - 1).replace(/-/g, '/')) - xList[0]; for (var i = 1; i < len; i++) { xList[i] = x + boundaryGap + width * (new Date(_getName(i).replace(/-/g, '/')) - xList[0]) / xList[len - 1]; } xList[0] = x + boundaryGap; } } else { xList.push(x + width / 2); } var list = []; var curSymbol; var n; var isEmpty; var textAlign; var rotation; for (var i = 0; i < len; i++) { x = xList[i]; curSymbol = (data[i].symbol && data[i].symbol.toLowerCase()) || symbol; if (curSymbol.match('empty')) { curSymbol = curSymbol.replace('empty', ''); isEmpty = true; } else { isEmpty = false; } if (curSymbol.match('star')) { n = (curSymbol.replace('star','') - 0) || 5; curSymbol = 'star'; } dataTextStyle = data[i].textStyle ? zrUtil.merge(data[i].textStyle || {}, textStyle) : textStyle; textAlign = dataTextStyle.align || 'center'; if (rotate) { textAlign = rotate > 0 ? 'right' : 'left'; rotation = [rotate * Math.PI / 180, x, y - 5]; } else { rotation = false; } list.push({ x: x, n: n, isEmpty: isEmpty, symbol: curSymbol, symbolSize: data[i].symbolSize || symbolSize, color: data[i].color, borderColor: data[i].borderColor, borderWidth: data[i].borderWidth, name: this._getReformedLabel(i), textColor: dataTextStyle.color, textAlign: textAlign, textBaseline: dataTextStyle.baseline || 'middle', textX: x, textY: y - (rotate ? 5 : 0), textFont: data[i].textStyle ? this.getFont(dataTextStyle) : textFont, rotation: rotation, showLabel: false }); } return list; }, _buildBackground: function () { var timelineOption = this.timelineOption; var padding = this.reformCssArray(this.timelineOption.padding); var width = this._location.width; var height = this._location.height; if (timelineOption.borderWidth !== 0 || timelineOption.backgroundColor.replace(/\s/g,'') != 'rgba(0,0,0,0)' ) { // 背景 this.shapeList.push(new RectangleShape({ zlevel: this.getZlevelBase(), z: this.getZBase(), hoverable :false, style: { x: this._location.x - padding[3], y: this._location.y - padding[0], width: width + padding[1] + padding[3], height: height + padding[0] + padding[2], brushType: timelineOption.borderWidth === 0 ? 'fill' : 'both', color: timelineOption.backgroundColor, strokeColor: timelineOption.borderColor, lineWidth: timelineOption.borderWidth } })); } }, _buildControl: function() { var self = this; var timelineOption = this.timelineOption; var lineStyle = timelineOption.lineStyle; var controlStyle = timelineOption.controlStyle; if (timelineOption.controlPosition === 'none') { return; } var iconSize = controlStyle.itemSize; var iconGap = controlStyle.itemGap; var x; if (timelineOption.controlPosition === 'left') { x = this._location.x; this._location.x += (iconSize + iconGap) * 3; } else { x = this._location.x2 - ((iconSize + iconGap) * 3 - iconGap); this._location.x2 -= (iconSize + iconGap) * 3; } var y = this._location.y; var iconStyle = { zlevel: this.getZlevelBase(), z: this.getZBase() + 1, style: { iconType: 'timelineControl', symbol: 'last', x: x, y: y, width: iconSize, height: iconSize, brushType: 'stroke', color: controlStyle.normal.color, strokeColor: controlStyle.normal.color, lineWidth: lineStyle.width }, highlightStyle: { color: controlStyle.emphasis.color, strokeColor: controlStyle.emphasis.color, lineWidth: lineStyle.width + 1 }, clickable: true }; this._ctrLastShape = new IconShape(iconStyle); this._ctrLastShape.onclick = function() { self.last(); }; this.shapeList.push(this._ctrLastShape); x += iconSize + iconGap; this._ctrPlayShape = new IconShape(zrUtil.clone(iconStyle)); this._ctrPlayShape.style.brushType = 'fill'; this._ctrPlayShape.style.symbol = 'play'; this._ctrPlayShape.style.status = this.timelineOption.autoPlay ? 'playing' : 'stop'; this._ctrPlayShape.style.x = x; this._ctrPlayShape.onclick = function() { if (self._ctrPlayShape.style.status === 'stop') { self.play(); } else { self.stop(); } }; this.shapeList.push(this._ctrPlayShape); x += iconSize + iconGap; this._ctrNextShape = new IconShape(zrUtil.clone(iconStyle)); this._ctrNextShape.style.symbol = 'next'; this._ctrNextShape.style.x = x; this._ctrNextShape.onclick = function() { self.next(); }; this.shapeList.push(this._ctrNextShape); }, /** * 构建时间轴 */ _buildChain: function () { var timelineOption = this.timelineOption; var lineStyle = timelineOption.lineStyle; this._timelineShae = { zlevel: this.getZlevelBase(), z: this.getZBase(), style: { x: this._location.x, y: this.subPixelOptimize(this._location.y, lineStyle.width), width: this._location.x2 - this._location.x, height: this._location.height, chainPoint: this._chainPoint, brushType:'both', strokeColor: lineStyle.color, lineWidth: lineStyle.width, lineType: lineStyle.type }, hoverable: false, clickable: true, onclick: this._onclick }; this._timelineShae = new ChainShape(this._timelineShae); this.shapeList.push(this._timelineShae); }, /** * 构建拖拽手柄 */ _buildHandle: function () { var curPoint = this._chainPoint[this.currentIndex]; var symbolSize = curPoint.symbolSize + 1; symbolSize = symbolSize < 5 ? 5 : symbolSize; this._handleShape = { zlevel: this.getZlevelBase(), z: this.getZBase() + 1, hoverable: false, draggable: true, style: { iconType: 'diamond', n: curPoint.n, x: curPoint.x - symbolSize, y: this._location.y + this._location.height / 4 - symbolSize, width: symbolSize * 2, height: symbolSize * 2, brushType:'both', textPosition: 'specific', textX: curPoint.x, textY: this._location.y - this._location.height / 4, textAlign: 'center', textBaseline: 'middle' }, highlightStyle: {}, ondrift: this._ondrift, ondragend: this._ondragend }; this._handleShape = new IconShape(this._handleShape); this.shapeList.push(this._handleShape); }, /** * 同步拖拽图形样式 */ _syncHandleShape: function() { if (!this.timelineOption.show) { return; } var timelineOption = this.timelineOption; var cpStyle = timelineOption.checkpointStyle; var curPoint = this._chainPoint[this.currentIndex]; this._handleShape.style.text = cpStyle.label.show ? curPoint.name : ''; this._handleShape.style.textFont = curPoint.textFont; this._handleShape.style.n = curPoint.n; if (cpStyle.symbol === 'auto') { this._handleShape.style.iconType = curPoint.symbol != 'none' ? curPoint.symbol : 'diamond'; } else { this._handleShape.style.iconType = cpStyle.symbol; if (cpStyle.symbol.match('star')) { this._handleShape.style.n = (cpStyle.symbol.replace('star','') - 0) || 5; this._handleShape.style.iconType = 'star'; } } var symbolSize; if (cpStyle.symbolSize === 'auto') { symbolSize = curPoint.symbolSize + 2; symbolSize = symbolSize < 5 ? 5 : symbolSize; } else { symbolSize = cpStyle.symbolSize - 0; } this._handleShape.style.color = cpStyle.color === 'auto' ? (curPoint.color ? curPoint.color : timelineOption.controlStyle.emphasis.color ) : cpStyle.color; this._handleShape.style.textColor = cpStyle.label.textStyle.color === 'auto' ? this._handleShape.style.color : cpStyle.label.textStyle.color; this._handleShape.highlightStyle.strokeColor = this._handleShape.style.strokeColor = cpStyle.borderColor === 'auto' ? (curPoint.borderColor ? curPoint.borderColor : '#fff') : cpStyle.borderColor; this._handleShape.style.lineWidth = cpStyle.borderWidth === 'auto' ? (curPoint.borderWidth ? curPoint.borderWidth : 0) : (cpStyle.borderWidth - 0); this._handleShape.highlightStyle.lineWidth = this._handleShape.style.lineWidth + 1; this.zr.animate(this._handleShape.id, 'style') .when( 500, { x: curPoint.x - symbolSize, textX: curPoint.x, y: this._location.y + this._location.height / 4 - symbolSize, width: symbolSize * 2, height: symbolSize * 2 } ) .start('ExponentialOut'); }, _findChainIndex: function(x) { var chainPoint = this._chainPoint; var len = chainPoint.length; if (x <= chainPoint[0].x) { return 0; } else if (x >= chainPoint[len - 1].x) { return len - 1; } for (var i = 0; i < len - 1; i++) { if (x >= chainPoint[i].x && x <= chainPoint[i + 1].x) { // catch you! return (Math.abs(x - chainPoint[i].x) < Math.abs(x - chainPoint[i + 1].x)) ? i : (i + 1); } } }, __onclick: function(param) { var x = zrEvent.getX(param.event); var newIndex = this._findChainIndex(x); if (newIndex === this.currentIndex) { return true; // 啥事都没发生 } this.currentIndex = newIndex; this.timelineOption.autoPlay && this.stop(); // 停止自动播放 clearTimeout(this.playTicket); this._onFrame(); }, /** * 拖拽范围控制 */ __ondrift: function (shape, dx) { this.timelineOption.autoPlay && this.stop(); // 停止自动播放 var chainPoint = this._chainPoint; var len = chainPoint.length; var newIndex; if (shape.style.x + dx <= chainPoint[0].x - chainPoint[0].symbolSize) { shape.style.x = chainPoint[0].x - chainPoint[0].symbolSize; newIndex = 0; } else if (shape.style.x + dx >= chainPoint[len - 1].x - chainPoint[len - 1].symbolSize) { shape.style.x = chainPoint[len - 1].x - chainPoint[len - 1].symbolSize; newIndex = len - 1; } else { shape.style.x += dx; newIndex = this._findChainIndex(shape.style.x); } var curPoint = chainPoint[newIndex]; var symbolSize = curPoint.symbolSize + 2; shape.style.iconType = curPoint.symbol; shape.style.n = curPoint.n; shape.style.textX = shape.style.x + symbolSize / 2; shape.style.y = this._location.y + this._location.height / 4 - symbolSize; shape.style.width = symbolSize * 2; shape.style.height = symbolSize * 2; shape.style.text = curPoint.name; //console.log(newIndex) if (newIndex === this.currentIndex) { return true; // 啥事都没发生 } this.currentIndex = newIndex; if (this.timelineOption.realtime) { clearTimeout(this.playTicket); var self = this; this.playTicket = setTimeout(function() { self._setCurrentOption(); },200); } return true; }, __ondragend: function () { this.isDragend = true; }, /** * 数据项被拖拽出去 */ ondragend: function (param, status) { if (!this.isDragend || !param.target) { // 没有在当前实例上发生拖拽行为则直接返回 return; } !this.timelineOption.realtime && this._setCurrentOption(); // 别status = {}赋值啊!! status.dragOut = true; status.dragIn = true; status.needRefresh = false; // 会有消息触发fresh,不用再刷一遍 // 处理完拖拽事件后复位 this.isDragend = false; this._syncHandleShape(); return; }, last: function () { this.timelineOption.autoPlay && this.stop(); // 停止自动播放 this.currentIndex -= 1; if (this.currentIndex < 0) { this.currentIndex = this.timelineOption.data.length - 1; } this._onFrame(); return this.currentIndex; }, next: function () { this.timelineOption.autoPlay && this.stop(); // 停止自动播放 this.currentIndex += 1; if (this.currentIndex >= this.timelineOption.data.length) { this.currentIndex = 0; } this._onFrame(); return this.currentIndex; }, play: function (targetIndex, autoPlay) { if (this._ctrPlayShape && this._ctrPlayShape.style.status != 'playing') { this._ctrPlayShape.style.status = 'playing'; this.zr.modShape(this._ctrPlayShape.id); this.zr.refreshNextFrame(); } this.timelineOption.autoPlay = autoPlay != null ? autoPlay : true; if (!this.timelineOption.autoPlay) { clearTimeout(this.playTicket); } this.currentIndex = targetIndex != null ? targetIndex : (this.currentIndex + 1); if (this.currentIndex >= this.timelineOption.data.length) { this.currentIndex = 0; } this._onFrame(); return this.currentIndex; }, stop: function () { if (this._ctrPlayShape && this._ctrPlayShape.style.status != 'stop') { this._ctrPlayShape.style.status = 'stop'; this.zr.modShape(this._ctrPlayShape.id); this.zr.refreshNextFrame(); } this.timelineOption.autoPlay = false; clearTimeout(this.playTicket); return this.currentIndex; }, /** * 避免dataZoom带来两次refresh,不设refresh接口,resize重复一下buildshape逻辑 */ resize: function () { if (this.timelineOption.show) { this.clear(); this._buildShape(); this._syncHandleShape(); } }, setTheme: function(needRefresh) { this.timelineOption = this.reformOption(zrUtil.clone(this.option.timeline)); // 通用字体设置 this.timelineOption.label.textStyle = this.getTextStyle( this.timelineOption.label.textStyle ); this.timelineOption.checkpointStyle.label.textStyle = this.getTextStyle( this.timelineOption.checkpointStyle.label.textStyle ); if (!this.myChart.canvasSupported) { // 不支持Canvas的强制关闭实时动画 this.timelineOption.realtime = false; } if (this.timelineOption.show && needRefresh) { this.clear(); this._buildShape(); this._syncHandleShape(); } }, /** * 释放后实例不可用,重载基类方法 */ onbeforDispose: function () { clearTimeout(this.playTicket); } }; function timelineControl(ctx, style) { var lineWidth = 2;//style.lineWidth; var x = style.x + lineWidth; var y = style.y + lineWidth + 2; var width = style.width - lineWidth; var height = style.height - lineWidth; var symbol = style.symbol; if (symbol === 'last') { ctx.moveTo(x + width - 2, y + height / 3); ctx.lineTo(x + width - 2, y); ctx.lineTo(x + 2, y + height / 2); ctx.lineTo(x + width - 2, y + height); ctx.lineTo(x + width - 2, y + height / 3 * 2); ctx.moveTo(x, y); ctx.lineTo(x, y); } else if (symbol === 'next') { ctx.moveTo(x + 2, y + height / 3); ctx.lineTo(x + 2, y); ctx.lineTo(x + width - 2, y + height / 2); ctx.lineTo(x + 2, y + height); ctx.lineTo(x + 2, y + height / 3 * 2); ctx.moveTo(x, y); ctx.lineTo(x, y); } else if (symbol === 'play') { if (style.status === 'stop') { ctx.moveTo(x + 2, y); ctx.lineTo(x + width - 2, y + height / 2); ctx.lineTo(x + 2, y + height); ctx.lineTo(x + 2, y); } else { var delta = style.brushType === 'both' ? 2 : 3; ctx.rect(x + 2, y, delta, height); ctx.rect(x + width - delta - 2, y, delta, height); } } else if (symbol.match('image')) { var imageLocation = ''; imageLocation = symbol.replace( new RegExp('^image:\\/\\/'), '' ); symbol = IconShape.prototype.iconLibrary.image; symbol(ctx, { x: x, y: y, width: width, height: height, image: imageLocation }); } } IconShape.prototype.iconLibrary['timelineControl'] = timelineControl; zrUtil.inherits(Timeline, Base); require('../component').define('timeline', Timeline); return Timeline; });