/** | |
* echarts组件:提示框 | |
* Copyright 2013 Baidu Inc. All rights reserved. | |
* | |
* @desc echarts基于Canvas,纯Javascript图表库,提供直观,生动,可交互,可个性化定制的数据统计图表。 | |
* @author Kener (@Kener-林峰, linzhifeng@baidu.com) | |
* | |
*/ | |
define(function (require) { | |
/** | |
* 构造函数 | |
* @param {Object} messageCenter echart消息中心 | |
* @param {ZRender} zr zrender实例 | |
* @param {Object} option 提示框参数 | |
* @param {HtmlElement} dom 目标对象 | |
*/ | |
function Tooltip(messageCenter, zr, option, dom) { | |
var Base = require('./base'); | |
Base.call(this, zr); | |
var ecConfig = require('../config'); | |
var ecData = require('../util/ecData'); | |
var zrConfig = require('zrender/config'); | |
var zrShape = require('zrender/shape'); | |
var zrEvent = require('zrender/tool/event'); | |
var zrArea = require('zrender/tool/area'); | |
var zrColor = require('zrender/tool/color'); | |
var zrUtil = require('zrender/tool/util'); | |
var rectangle = zrShape.get('rectangle'); | |
var self = this; | |
self.type = ecConfig.COMPONENT_TYPE_TOOLTIP; | |
var _zlevelBase = self.getZlevelBase(); | |
var component = {}; // 组件索引 | |
var grid; | |
var xAxis; | |
var yAxis; | |
// tooltip dom & css | |
var _tDom = document.createElement('div'); | |
// 通用样式 | |
var _gCssText = 'position:absolute;' | |
+ 'display:block;' | |
+ 'transition:left 1s,top 1s;' | |
+ '-moz-transition:left 1s,top 1s;' | |
+ '-webkit-transition:left 1s,top 1s;' | |
+ '-o-transition:left 1s,top 1s;' | |
+ 'border-style:solid;'; | |
// 默认样式 | |
var _defaultCssText; // css样式缓存 | |
var _needAxisTrigger; // 坐标轴触发 | |
var _hidingTicket; | |
var _hideDelay = 100; // 隐藏延迟 | |
var _showingTicket; | |
var _showDelay = 30; // 显示延迟 | |
var _curTarget; | |
var _event; | |
var _curTicket; // 异步回调标识,用来区分多个请求 | |
// 缓存一些高宽数据 | |
var _zrHeight = zr.getHeight(); | |
var _zrWidth = zr.getWidth(); | |
var _axisLineShape = { | |
shape : 'line', | |
id : zr.newShapeId('tooltip'), | |
zlevel: _zlevelBase, | |
invisible : true, | |
hoverable: false, | |
style : { | |
lineWidth : 2, | |
strokeColor : ecConfig.categoryAxis.axisLine.lineStyle.color | |
} | |
}; | |
zr.addShape(_axisLineShape); | |
/** | |
* 根据配置设置dom样式 | |
*/ | |
function _style(opt) { | |
if (!opt) { | |
return ''; | |
} | |
cssText = []; | |
if (opt.backgroundColor) { | |
// for sb ie~ | |
cssText.push( | |
'background-Color:' + zrColor.toHex( | |
opt.backgroundColor | |
) | |
); | |
cssText.push('filter:alpha(opacity=70)'); | |
cssText.push('background-Color:' + opt.backgroundColor); | |
} | |
if (typeof opt.borderWidth != 'undefined') { | |
cssText.push('border-width:' + opt.borderWidth + 'px'); | |
} | |
if (typeof opt.borderColor != 'undefined') { | |
cssText.push('border-color:' + opt.borderColor + 'px'); | |
} | |
if (typeof opt.borderRadius != 'undefined') { | |
cssText.push( | |
'border-radius:' + opt.borderRadius + 'px' | |
); | |
cssText.push( | |
'-moz-border-radius:' + opt.borderRadius + 'px' | |
); | |
cssText.push( | |
'-webkit-border-radius:' + opt.borderRadius + 'px' | |
); | |
cssText.push( | |
'-o-border-radius:' + opt.borderRadius + 'px' | |
); | |
} | |
var textStyle = opt.textStyle; | |
if (textStyle) { | |
textStyle.color && cssText.push('color:' + textStyle.color); | |
textStyle.decoration && cssText.push( | |
'text-decoration:' + textStyle.decoration | |
); | |
textStyle.align && cssText.push( | |
'text-align:' + textStyle.align | |
); | |
textStyle.fontFamily && cssText.push( | |
'font-family:' + textStyle.fontFamily | |
); | |
textStyle.fontSize && cssText.push( | |
'font-size:' + textStyle.fontSize + 'px' | |
); | |
textStyle.fontSize && cssText.push( | |
'line-height:' + Math.round(textStyle.fontSize*3/2) + 'px' | |
); | |
textStyle.fontStyle && cssText.push( | |
'font-style:' + textStyle.fontStyle | |
); | |
textStyle.fontWeight && cssText.push( | |
'font-weight:' + textStyle.fontWeight | |
); | |
} | |
var padding = opt.padding; | |
if (typeof padding != 'undefined') { | |
padding = self.reformCssArray(padding); | |
cssText.push( | |
'padding:' + padding[0] + 'px ' | |
+ padding[1] + 'px ' | |
+ padding[2] + 'px ' | |
+ padding[3] + 'px' | |
); | |
} | |
cssText = cssText.join(';') + ';'; | |
return cssText; | |
} | |
function _hide() { | |
if (_tDom) { | |
_tDom.style.display = 'none'; | |
} | |
if (!_axisLineShape.invisible) { | |
_axisLineShape.invisible = true; | |
zr.modShape(_axisLineShape.id, _axisLineShape); | |
zr.refresh(); | |
} | |
} | |
function _show(x, y, specialCssText) { | |
var domHeight = _tDom.offsetHeight; | |
var domWidth = _tDom.offsetWidth; | |
if (x + domWidth > _zrWidth) { | |
x = _zrWidth - domWidth; | |
} | |
if (y + domHeight > _zrHeight) { | |
y = _zrHeight - domHeight; | |
} | |
_tDom.style.cssText = _gCssText | |
+ _defaultCssText | |
+ (specialCssText ? specialCssText : '') | |
+ 'left:' + x + 'px;top:' + y + 'px;'; | |
} | |
function _tryShow() { | |
var needShow; | |
var trigger; | |
if (!_curTarget) { | |
// 坐标轴事件 | |
_findAxisTrigger(); | |
} | |
else { | |
// 数据项事件 | |
if (_curTarget._type == 'island' | |
&& self.deepQuery([option], 'tooltip.show') | |
) { | |
_showItemTrigger(); | |
return; | |
} | |
var serie = ecData.get(_curTarget, 'series'); | |
var data = ecData.get(_curTarget, 'data'); | |
needShow = self.deepQuery( | |
[data, serie, option], | |
'tooltip.show' | |
); | |
if (typeof serie == 'undefined' | |
|| typeof data == 'undefined' | |
|| needShow === false | |
) { | |
// 不响应tooltip的数据对象延时隐藏 | |
clearTimeout(_hidingTicket); | |
clearTimeout(_showingTicket); | |
_hidingTicket = setTimeout(_hide, _hideDelay); | |
} | |
else { | |
trigger = self.deepQuery( | |
[data, serie, option], | |
'tooltip.trigger' | |
); | |
trigger == 'axis' | |
? _showAxisTrigger( | |
serie.xAxisIndex, serie.yAxisIndex, | |
ecData.get(_curTarget, 'dataIndex') | |
) | |
: _showItemTrigger(); | |
} | |
} | |
} | |
function _findAxisTrigger() { | |
var series = option.series; | |
var xAxisIndex; | |
var yAxisIndex; | |
for (var i = 0, l = series.length; i < l; i++) { | |
// 找到第一个axis触发tooltip的系列 | |
if (self.deepQuery( | |
[series[i], option], 'tooltip.trigger' | |
) == 'axis' | |
) { | |
xAxisIndex = series[i].xAxisIndex || 0; | |
yAxisIndex = series[i].yAxisIndex || 0; | |
if (xAxis.getAxis(xAxisIndex) | |
&& xAxis.getAxis(xAxisIndex).type | |
== ecConfig.COMPONENT_TYPE_AXIS_CATEGORY | |
) { | |
// 横轴为类目轴 | |
_showAxisTrigger(xAxisIndex, yAxisIndex, | |
_getNearestDataIndex('x', xAxis.getAxis(xAxisIndex)) | |
); | |
return; | |
} else if (yAxis.getAxis(yAxisIndex) | |
&& yAxis.getAxis(yAxisIndex).type | |
== ecConfig.COMPONENT_TYPE_AXIS_CATEGORY | |
) { | |
// 纵轴为类目轴 | |
_showAxisTrigger(xAxisIndex, yAxisIndex, | |
_getNearestDataIndex('y', yAxis.getAxis(yAxisIndex)) | |
); | |
return; | |
} | |
} | |
} | |
} | |
/** | |
* 根据坐标轴事件带的属性获取最近的axisDataIndex | |
*/ | |
function _getNearestDataIndex(direction, categoryAxis) { | |
var dataIndex = -1; | |
var x = zrEvent.getX(_event); | |
var y = zrEvent.getY(_event); | |
if (direction == 'x') { | |
// 横轴为类目轴 | |
var left; | |
var right; | |
var xEnd = grid.getXend(); | |
var curCoord = categoryAxis.getCoordByIndex(dataIndex); | |
while (curCoord < xEnd) { | |
if (curCoord <= x) { | |
left = curCoord; | |
} | |
if (curCoord >= x) { | |
break; | |
} | |
curCoord = categoryAxis.getCoordByIndex(++dataIndex); | |
right = curCoord; | |
} | |
if (x - left < right - x) { | |
dataIndex -= 1; | |
} | |
else { | |
// 离右边近,看是否为最后一个 | |
if (typeof categoryAxis.getNameByIndex(dataIndex) | |
== 'undefined' | |
) { | |
dataIndex = -1; | |
} | |
} | |
return dataIndex; | |
} | |
else { | |
// 纵轴为类目轴 | |
var top; | |
var bottom; | |
var yStart = grid.getY(); | |
var curCoord = categoryAxis.getCoordByIndex(dataIndex); | |
while (curCoord > yStart) { | |
if (curCoord >= y) { | |
bottom = curCoord; | |
} | |
if (curCoord <= y) { | |
break; | |
} | |
curCoord = categoryAxis.getCoordByIndex(++dataIndex); | |
top = curCoord; | |
} | |
if (y - top > bottom - y) { | |
dataIndex -= 1; | |
} | |
else { | |
// 离上方边近,看是否为最后一个 | |
if (typeof categoryAxis.getNameByIndex(dataIndex) | |
== 'undefined' | |
) { | |
dataIndex = -1; | |
} | |
} | |
return dataIndex; | |
} | |
return -1; | |
} | |
function _showAxisTrigger(xAxisIndex, yAxisIndex, dataIndex) { | |
if (typeof xAxis == 'undefined' | |
|| typeof yAxis == 'undefined' | |
|| typeof xAxisIndex == 'undefined' | |
|| typeof yAxisIndex == 'undefined' | |
|| dataIndex < 0 | |
) { | |
// 不响应tooltip的数据对象延时隐藏 | |
clearTimeout(_hidingTicket); | |
clearTimeout(_showingTicket); | |
_hidingTicket = setTimeout(_hide, _hideDelay); | |
return; | |
} | |
var series = option.series; | |
var seriesArray = []; | |
var categoryAxis; | |
var x; | |
var y; | |
var formatter; | |
var specialCssText = ''; | |
if (self.deepQuery([option], 'tooltip.trigger') == 'axis') { | |
if (self.deepQuery([option], 'tooltip.show') === false) { | |
return; | |
} | |
formatter = self.deepQuery([option],'tooltip.formatter'); | |
} | |
if (xAxisIndex != -1 | |
&& xAxis.getAxis(xAxisIndex).type | |
== ecConfig.COMPONENT_TYPE_AXIS_CATEGORY | |
) { | |
// 横轴为类目轴,找到所有用这条横轴并且axis触发的系列数据 | |
categoryAxis = xAxis.getAxis(xAxisIndex); | |
for (var i = 0, l = series.length; i < l; i++) { | |
if (series[i].xAxisIndex == xAxisIndex | |
&& self.deepQuery( | |
[series[i], option], 'tooltip.trigger' | |
) == 'axis' | |
) { | |
formatter = self.deepQuery( | |
[series[i]], | |
'tooltip.formatter' | |
) || formatter; | |
specialCssText += _style(self.deepQuery( | |
[series[i]], 'tooltip' | |
)); | |
seriesArray.push(series[i]); | |
} | |
} | |
y = zrEvent.getY(_event) + 10; | |
x = categoryAxis.getCoordByIndex(dataIndex); | |
_axisLineShape.style.xStart = x; | |
_axisLineShape.style.yStart = component.grid.getY(); | |
_axisLineShape.style.xEnd = x; | |
_axisLineShape.style.yEnd = component.grid.getYend(); | |
x += 10; | |
} | |
else if (yAxisIndex != -1 | |
&& yAxis.getAxis(yAxisIndex).type | |
== ecConfig.COMPONENT_TYPE_AXIS_CATEGORY | |
) { | |
// 纵轴为类目轴,找到所有用这条纵轴并且axis触发的系列数据 | |
categoryAxis = yAxis.getAxis(yAxisIndex); | |
for (var i = 0, l = series.length; i < l; i++) { | |
if (series[i].yAxisIndex == yAxisIndex | |
&& self.deepQuery( | |
[series[i], option], 'tooltip.trigger' | |
) == 'axis' | |
) { | |
formatter = self.deepQuery( | |
[series[i]], | |
'tooltip.formatter' | |
) || formatter; | |
specialCssText += _style(self.deepQuery( | |
[series[i]], 'tooltip' | |
)); | |
seriesArray.push(series[i]); | |
} | |
} | |
x = zrEvent.getX(_event) + 10; | |
y = categoryAxis.getCoordByIndex(dataIndex); | |
_axisLineShape.style.xStart = component.grid.getX(); | |
_axisLineShape.style.yStart = y; | |
_axisLineShape.style.xEnd = component.grid.getXend(); | |
_axisLineShape.style.yEnd = y; | |
y += 10; | |
} | |
if (seriesArray.length > 0) { | |
var data; | |
if (typeof formatter == 'function') { | |
var params = []; | |
for (var i = 0, l = seriesArray.length; i < l; i++) { | |
data = seriesArray[i].data[dataIndex] || '-'; | |
data = typeof data.value != 'undefined' | |
? data.value : data; | |
params.push([ | |
seriesArray[i].name, | |
categoryAxis.getNameByIndex(dataIndex), | |
data | |
]); | |
} | |
_curTicket = 'axis:' + dataIndex; | |
_tDom.innerHTML = formatter( | |
params, _curTicket, _setContent | |
); | |
} | |
else if (typeof formatter == 'string') { | |
formatter = formatter.replace('{a}','{a0}') | |
.replace('{b}','{b0}') | |
.replace('{c}','{c0}'); | |
for (var i = 0, l = seriesArray.length; i < l; i++) { | |
formatter = formatter.replace( | |
'{a' + i + '}', | |
seriesArray[i].name | |
); | |
formatter = formatter.replace( | |
'{b' + i + '}', | |
categoryAxis.getNameByIndex(dataIndex) | |
); | |
data = seriesArray[i].data[dataIndex] || '-'; | |
data = typeof data.value != 'undefined' | |
? data.value : data; | |
formatter = formatter.replace( | |
'{c' + i + '}', | |
data | |
); | |
} | |
_tDom.innerHTML = formatter; | |
} | |
else { | |
formatter = categoryAxis.getNameByIndex(dataIndex); | |
for (var i = 0, l = seriesArray.length; i < l; i++) { | |
formatter += '<br/>' + seriesArray[i].name + ' : '; | |
data = seriesArray[i].data[dataIndex] || '-'; | |
data = typeof data.value != 'undefined' | |
? data.value : data; | |
formatter += data; | |
} | |
_tDom.innerHTML = formatter; | |
} | |
if (!self.hasAppend) { | |
_tDom.style.left = _zrWidth / 2 + 'px'; | |
_tDom.style.top = _zrHeight / 2 + 'px'; | |
dom.firstChild.appendChild(_tDom); | |
self.hasAppend = true; | |
} | |
_show(x, y, specialCssText); | |
_axisLineShape.invisible = false; | |
zr.modShape(_axisLineShape.id, _axisLineShape); | |
zr.refresh(); | |
} | |
} | |
function _showItemTrigger() { | |
var serie = ecData.get(_curTarget, 'series'); | |
var data = ecData.get(_curTarget, 'data'); | |
var name = ecData.get(_curTarget, 'name'); | |
var value = ecData.get(_curTarget, 'value'); | |
var speical = ecData.get(_curTarget, 'special'); | |
// 从低优先级往上找到trigger为item的formatter和样式 | |
var formatter; | |
var specialCssText = ''; | |
if (_curTarget._type != 'island') { | |
// 全局 | |
if (self.deepQuery([option], 'tooltip.trigger') == 'item' | |
) { | |
formatter = self.deepQuery( | |
[option], 'tooltip.formatter' | |
) || formatter; | |
} | |
// 系列 | |
if (self.deepQuery([serie], 'tooltip.trigger') == 'item' | |
) { | |
formatter = self.deepQuery( | |
[serie], 'tooltip.formatter' | |
) || formatter; | |
specialCssText += _style(self.deepQuery( | |
[serie], 'tooltip' | |
)); | |
} | |
// 数据项 | |
formatter = self.deepQuery( | |
[data], 'tooltip.formatter' | |
) || formatter; | |
specialCssText += _style(self.deepQuery([data], 'tooltip')); | |
} else { | |
formatter = self.deepQuery( | |
[data, serie, option], | |
'tooltip.islandFormatter' | |
); | |
} | |
if (typeof formatter == 'function') { | |
_curTicket = serie.name | |
+ ':' | |
+ ecData.get(_curTarget, 'dataIndex'); | |
_tDom.innerHTML = formatter( | |
[ | |
serie.name, | |
name, | |
value, | |
speical | |
], | |
_curTicket, | |
_setContent | |
); | |
} | |
else if (typeof formatter == 'string') { | |
formatter = formatter.replace('{a}','{a0}') | |
.replace('{b}','{b0}') | |
.replace('{c}','{c0}') | |
.replace('{d}','{d0}'); | |
formatter = formatter.replace('{a0}', serie.name) | |
.replace('{b0}', name) | |
.replace('{c0}', value); | |
if (typeof speical != 'undefined') { | |
formatter = formatter.replace('{d0}', speical); | |
} | |
_tDom.innerHTML = formatter; | |
} | |
else { | |
_tDom.innerHTML = serie.name + '<br/>' + | |
name + ' : ' + value + | |
(typeof speical == 'undefined' | |
? '' | |
: (' (' + speical + ')')); | |
} | |
if (!self.hasAppend) { | |
_tDom.style.left = _zrWidth / 2 + 'px'; | |
_tDom.style.top = _zrHeight / 2 + 'px'; | |
dom.firstChild.appendChild(_tDom); | |
self.hasAppend = true; | |
} | |
_show( | |
zrEvent.getX(_event) + 20, | |
zrEvent.getY(_event) - 20, | |
specialCssText | |
); | |
if (!_axisLineShape.invisible) { | |
_axisLineShape.invisible = true; | |
zr.modShape(_axisLineShape.id, _axisLineShape); | |
zr.refresh(); | |
} | |
} | |
/** | |
* zrender事件响应:窗口大小改变 | |
*/ | |
function _onresize() { | |
_zrHeight = zr.getHeight(); | |
_zrWidth = zr.getWidth(); | |
} | |
/** | |
* zrender事件响应:鼠标移动 | |
*/ | |
function _onmousemove(param) { | |
clearTimeout(_hidingTicket); | |
clearTimeout(_showingTicket); | |
var target = param.target; | |
if (!target) { | |
// 判断是否落到直角系里,axis触发的tooltip | |
if (_needAxisTrigger | |
&& zrArea.isInside( | |
rectangle, | |
grid.getArea(), | |
zrEvent.getX(param.event), | |
zrEvent.getY(param.event) | |
) | |
) { | |
_curTarget = false; | |
_event = param.event; | |
_event._target = _event.target || _event.toElement; | |
_event.zrenderX = zrEvent.getX(_event); | |
_event.zrenderY = zrEvent.getY(_event); | |
_showingTicket = setTimeout(_tryShow, _showDelay); | |
} | |
else { | |
_hidingTicket = setTimeout(_hide, _hideDelay); | |
} | |
} | |
else { | |
_curTarget = target; | |
_event = param.event; | |
_event._target = _event.target || _event.toElement; | |
_event.zrenderX = zrEvent.getX(_event); | |
_event.zrenderY = zrEvent.getY(_event); | |
_showingTicket = setTimeout(_tryShow, _showDelay); | |
} | |
} | |
/** | |
* 异步回调填充内容 | |
*/ | |
function _setContent(ticket, content) { | |
if (ticket == _curTicket) { | |
_tDom.innerHTML = content; | |
} | |
var cssText = ''; | |
var domHeight = _tDom.offsetHeight; | |
var domWidth = _tDom.offsetWidth; | |
if (_tDom.offsetLeft + domWidth > _zrWidth) { | |
cssText += 'left:' + (_zrWidth - domWidth) + 'px;'; | |
} | |
if (_tDom.offsetTop + domHeight > _zrHeight) { | |
cssText += 'top:' + (_zrHeight - domHeight) + 'px;'; | |
} | |
if (cssText !== '') { | |
_tDom.style.cssText += cssText; | |
} | |
} | |
function setComponent(newComponent) { | |
component = newComponent; | |
grid = component.grid; | |
xAxis = component.xAxis; | |
yAxis = component.yAxis; | |
} | |
function init(newOption, newDom) { | |
option = newOption; | |
dom = newDom; | |
option.tooltip = self.reformOption(option.tooltip); | |
option.tooltip.textStyle = zrUtil.merge( | |
option.tooltip.textStyle, | |
ecConfig.textStyle, | |
{ | |
'overwrite': false, | |
'recursive': true | |
} | |
); | |
// 补全padding属性 | |
option.tooltip.padding = self.reformCssArray( | |
option.tooltip.padding | |
); | |
_needAxisTrigger = false; | |
if (option.tooltip.trigger == 'axis') { | |
_needAxisTrigger = true; | |
} | |
var series = option.series; | |
for (var i = 0, l = series.length; i < l; i++) { | |
if (self.deepQuery([series[i]], 'tooltip.trigger') | |
== 'axis' | |
) { | |
_needAxisTrigger = true; | |
break; | |
} | |
} | |
_defaultCssText = _style(option.tooltip); | |
_tDom.style.position = 'absolute'; // 不是多余的,别删! | |
self.hasAppend = false; | |
} | |
/** | |
* 释放后实例不可用,重载基类方法 | |
*/ | |
function dispose() { | |
clearTimeout(_hidingTicket); | |
clearTimeout(_showingTicket); | |
zr.un(zrConfig.EVENT.RESIZE, _onresize); | |
zr.un(zrConfig.EVENT.MOUSEMOVE, _onmousemove); | |
if (self.hasAppend) { | |
dom.firstChild.removeChild(_tDom); | |
} | |
_tDom = null; | |
// self.clear(); | |
self.shapeList = null; | |
self = null; | |
} | |
zr.on(zrConfig.EVENT.RESIZE, _onresize); | |
zr.on(zrConfig.EVENT.MOUSEMOVE, _onmousemove); | |
// 重载基类方法 | |
self.dispose = dispose; | |
self.init = init; | |
self.setComponent = setComponent; | |
init(option, dom); | |
} | |
return Tooltip; | |
}); |