/**
* @fileOverview The class of tooltip
* @author sima.zhang
*/
const Util = require('../../util');
const Base = require('../../base');
const Global = require('../../global');
const DomUtil = Util.DomUtil;
const CONTAINER_CLASS = 'g2-tooltip';
const TITLE_CLASS = 'g2-tooltip-title';
const LIST_CLASS = 'g2-tooltip-list';
const MARKER_CLASS = 'g2-tooltip-marker';
const VALUE_CLASS = 'g2-tooltip-value';
const LIST_ITEM_CLASS = 'g2-tooltip-list-item';
function find(dom, cls) {
return dom.getElementsByClassName(cls)[0];
}
function refixTooltipPosition(x, y, el, viewWidth, viewHeight) {
const width = el.clientWidth;
const height = el.clientHeight;
const gap = 20;
if (x + width + gap > viewWidth) {
x -= width + gap;
x = x < 0 ? 0 : x;
} else {
x += gap;
}
if (y + height + gap > viewHeight) {
y -= height + gap;
y = x < 0 ? 0 : y;
} else {
y += gap;
}
return [ x, y ];
}
function calcTooltipPosition(x, y, position, dom, target) {
const domWidth = dom.clientWidth;
const domHeight = dom.clientHeight;
let rectWidth = 0;
let rectHeight = 0;
let gap = 20;
if (target) {
const rect = target.getBBox();
rectWidth = rect.width;
rectHeight = rect.height;
x = rect.x;
y = rect.y;
gap = 5;
}
switch (position) {
case 'inside':
x = x + rectWidth / 2 - domWidth / 2;
y = y + rectHeight / 2 - domHeight / 2;
break;
case 'top':
x = x + rectWidth / 2 - domWidth / 2;
y = y - domHeight - gap;
break;
case 'left':
x = x - domWidth - gap;
y = y + rectHeight / 2 - domHeight / 2;
break;
case 'right':
x = x + rectWidth + gap;
y = y + rectHeight / 2 - domHeight / 2;
break;
case 'bottom':
default:
x = x + rectWidth / 2 - domWidth / 2;
y = y + rectHeight + gap;
break;
}
return [ x, y ];
}
function confineTooltipPosition(x, y, el, plotRange, onlyHorizontal) {
const gap = 20;
const width = el.clientWidth;
const height = el.clientHeight;
if (x + width > plotRange.tr.x) {
x -= width + 2 * gap;
}
if (x < plotRange.tl.x) {
x = plotRange.tl.x;
}
if (!onlyHorizontal) {
if (y + height > plotRange.bl.y) {
y -= height + 2 * gap;
}
if (y < plotRange.tl.y) {
y = plotRange.tl.y;
}
}
return [ x, y ];
}
class Tooltip extends Base {
getDefaultCfg() {
return {
/**
* 右下角坐标
* @type {Number}
*/
x: 0,
/**
* y 右下角坐标
* @type {Number}
*/
y: 0,
/**
* tooltip 记录项
* @type {Array}
*/
items: null,
/**
* 是否展示 title
* @type {Boolean}
*/
showTitle: true,
/**
* tooltip 辅助线配置
* @type {Object}
*/
crosshairs: null,
/**
* 视图范围
* @type {Object}
*/
plotRange: null,
/**
* x轴上,移动到位置的偏移量
* @type {Number}
*/
offset: 10,
/**
* 时间戳
* @type {Number}
*/
timeStamp: 0,
/**
* tooltip 容器模板
* @type {String}
*/
containerTpl: '
',
/**
* tooltip 列表项模板
* @type {String}
*/
itemTpl: ''
+ ''
+ '{name}{value}',
/**
* 将 tooltip 展示在指定区域内
* @type {Boolean}
*/
inPlot: true,
/**
* tooltip 内容跟随鼠标移动
* @type {Boolean}
*/
follow: true,
/**
* 是否允许鼠标停留在 tooltip 上,默认不允许
* @type {Boolean}
*/
enterable: false,
/**
* 设置几何体对应的maker
*/
marker: null
};
}
_initTooltipWrapper() {
const self = this;
const containerTpl = self.get('containerTpl');
const outterNode = self.get('canvas').get('el').parentNode;
let container;
if (/^\#/.test(containerTpl)) { // 如果传入 dom 节点的 id
const id = containerTpl.replace('#', '');
container = document.getElementById(id);
} else {
container = DomUtil.createDom(containerTpl);
DomUtil.modifyCSS(container, self.get(CONTAINER_CLASS));
outterNode.appendChild(container);
outterNode.style.position = 'relative';
}
self.set('container', container);
}
_init() {
const crosshairs = this.get('crosshairs');
const frontPlot = this.get('frontPlot');
const backPlot = this.get('backPlot');
const viewTheme = this.get('viewTheme') || Global;
let crosshairsGroup;
if (crosshairs) {
if (crosshairs.type === 'rect') {
this.set('crosshairs', Util.deepMix({}, viewTheme.tooltipCrosshairsRect, crosshairs));
crosshairsGroup = backPlot.addGroup({
zIndex: 0
});
} else {
this.set('crosshairs', Util.deepMix({}, viewTheme.tooltipCrosshairsLine, crosshairs));
crosshairsGroup = frontPlot.addGroup();
}
}
this.set('crosshairsGroup', crosshairsGroup);
this._initTooltipWrapper();
}
constructor(cfg) {
super(cfg);
this._init(); // 初始化属性
if (this.get('items')) {
this._renderTooltip();
}
this._renderCrosshairs();
}
_clearDom() {
const container = this.get('container');
const titleDom = find(container, TITLE_CLASS);
const listDom = find(container, LIST_CLASS);
if (titleDom) {
titleDom.innerHTML = '';
}
if (listDom) {
listDom.innerHTML = '';
}
}
_addItem(item, index) {
const itemTpl = this.get('itemTpl'); // TODO: 有可能是个回调函数
const itemDiv = Util.substitute(itemTpl, Util.mix({
index
}, item));
const itemDOM = DomUtil.createDom(itemDiv);
DomUtil.modifyCSS(itemDOM, this.get(LIST_ITEM_CLASS));
const markerDom = find(itemDOM, MARKER_CLASS);
if (markerDom) {
DomUtil.modifyCSS(markerDom, this.get(MARKER_CLASS));
}
const valueDom = find(itemDOM, VALUE_CLASS);
if (valueDom) {
DomUtil.modifyCSS(valueDom, this.get(VALUE_CLASS));
}
return itemDOM;
}
_renderTooltip() {
const self = this;
const showTitle = self.get('showTitle');
const titleContent = self.get('titleContent');
const container = self.get('container');
const titleDom = find(container, TITLE_CLASS);
const listDom = find(container, LIST_CLASS);
const items = self.get('items');
self._clearDom();
if (titleDom && showTitle) {
DomUtil.modifyCSS(titleDom, self.get(TITLE_CLASS));
titleDom.innerHTML = titleContent;
}
if (listDom) {
DomUtil.modifyCSS(listDom, self.get(LIST_CLASS));
Util.each(items, (item, index) => {
listDom.appendChild(self._addItem(item, index));
});
}
}
_clearCrosshairsGroup() {
const crosshairsGroup = this.get('crosshairsGroup');
this.set('crossLineShapeX', null);
this.set('crossLineShapeY', null);
this.set('crosshairsRectShape', null);
crosshairsGroup.clear();
}
_renderCrosshairs() {
const crosshairs = this.get('crosshairs');
const canvas = this.get('canvas');
const plotRange = this.get('plotRange');
const isTransposed = this.get('isTransposed');
if (crosshairs) {
this._clearCrosshairsGroup();
switch (crosshairs.type) {
case 'x':
this._renderHorizontalLine(canvas, plotRange);
break;
case 'y':
this._renderVerticalLine(canvas, plotRange);
break;
case 'cross':
this._renderHorizontalLine(canvas, plotRange);
this._renderVerticalLine(canvas, plotRange);
break;
case 'rect':
this._renderBackground(canvas, plotRange);
break;
default:
isTransposed ? this._renderHorizontalLine(canvas, plotRange) : this._renderVerticalLine(canvas, plotRange);
}
}
}
_addCrossLineShape(attrs, type) {
const crosshairsGroup = this.get('crosshairsGroup');
const shape = crosshairsGroup.addShape('line', {
capture: false,
attrs
});
shape.hide();
this.set('crossLineShape' + type, shape);
return shape;
}
_renderVerticalLine(canvas, plotRange) {
const { style } = this.get('crosshairs');
const attrs = Util.mix({
x1: 0,
y1: plotRange ? plotRange.bl.y : canvas.get('height'),
x2: 0,
y2: plotRange ? plotRange.tl.y : 0
}, style);
this._addCrossLineShape(attrs, 'Y');
}
_renderHorizontalLine(canvas, plotRange) {
const { style } = this.get('crosshairs');
const attrs = Util.mix({
x1: plotRange ? plotRange.bl.x : canvas.get('width'),
y1: 0,
x2: plotRange ? plotRange.br.x : 0,
y2: 0
}, style);
this._addCrossLineShape(attrs, 'X');
}
_renderBackground(canvas, plotRange) {
const { style } = this.get('crosshairs');
const crosshairsGroup = this.get('crosshairsGroup');
const attrs = Util.mix({
x: plotRange ? plotRange.tl.x : 0,
y: plotRange ? plotRange.tl.y : canvas.get('height'),
width: plotRange ? plotRange.br.x - plotRange.bl.x : canvas.get('width'),
height: plotRange ? Math.abs(plotRange.tl.y - plotRange.bl.y) : canvas.get('height')
}, style);
const shape = crosshairsGroup.addShape('rect', {
attrs
});
shape.hide();
this.set('crosshairsRectShape', shape);
return shape;
}
isContentChange(title, items) {
const titleContent = this.get('titleContent');
const lastItems = this.get('items');
let isChanged = !(title === titleContent && lastItems.length === items.length);
if (!isChanged) {
Util.each(items, (item, index) => {
const preItem = lastItems[index];
for (const key in item) {
if (item.hasOwnProperty(key)) {
if (!Util.isObject(item[key]) && item[key] !== preItem[key]) {
isChanged = true;
break;
}
}
}
// isChanged = (item.value !== preItem.value) || (item.color !== preItem.color) || (item.name !== preItem.name) || (item.title !== preItem.title);
if (isChanged) {
return false;
}
});
}
return isChanged;
}
setContent(title, items) {
// const isChange = this.isContentChange(title, items);
// if (isChange) {
// 在外面进行判断是否内容发生改变
const timeStamp = +new Date();
this.set('items', items);
this.set('titleContent', title);
this.set('timeStamp', timeStamp);
this._renderTooltip();
// }
return this;
}
setMarkers(markerItems, markerCfg) {
const self = this;
let markerGroup = self.get('markerGroup');
const frontPlot = self.get('frontPlot');
if (!markerGroup) {
markerGroup = frontPlot.addGroup({
zIndex: 1,
capture: false // 不进行拾取
});
self.set('markerGroup', markerGroup);
} else {
markerGroup.clear();
}
Util.each(markerItems, item => {
markerGroup.addShape('marker', {
color: item.color,
attrs: Util.mix({
// fix: Theme.tooltipMarker invalid
fill: item.color,
symbol: 'circle',
shadowColor: item.color
}, markerCfg, {
x: item.x,
y: item.y,
symbol: item.symbol
})
});
});
this.set('markerItems', markerItems);
}
clearMarkers() {
const markerGroup = this.get('markerGroup');
markerGroup && markerGroup.clear();
}
setPosition(x, y, target) {
const container = this.get('container');
// 修复tooltip初始位置错误,是由于container隐藏时获取不到宽高导致
// 记住上次可见性
const lastVisibility = container.style.visibility;
const lastDisplay = container.style.display;
// 设为可见
container.style.visibility = 'visible';
container.style.display = 'block';
const crossLineShapeX = this.get('crossLineShapeX');
const crossLineShapeY = this.get('crossLineShapeY');
const crosshairsRectShape = this.get('crosshairsRectShape');
let endx = x;
let endy = y;
// const outterNode = this.get('canvas').get('el').parentNode;
const outterNode = this.get('canvas').get('el');
const viewWidth = DomUtil.getWidth(outterNode);
const viewHeight = DomUtil.getHeight(outterNode);
let offset = this.get('offset');
let position;
const prePosition = this.get('prePosition') || { x: 0, y: 0 };
if (this.get('position')) {
position = calcTooltipPosition(x, y, this.get('position'), container, target);
x = position[0];
y = position[1];
} else if (this.get('enterable')) {
y = y - container.clientHeight / 2;
position = { x, y };
if (prePosition && x - prePosition.x > 0) { // 留 1px 防止鼠标点击事件无法在画布上触发
x -= container.clientWidth + 1;
} else {
x += 1;
}
} else {
position = refixTooltipPosition(x, y, container, viewWidth, viewHeight);
x = position[0];
y = position[1];
}
this.set('prePosition', position); // 记录上次的位置
if (this.get('inPlot')) { // tooltip 必须限制在绘图区域内
const plotRange = this.get('plotRange');
position = confineTooltipPosition(x, y, container, plotRange, this.get('enterable'));
x = position[0];
y = position[1];
}
if (prePosition.x !== x || prePosition.y !== y) {
const markerItems = this.get('markerItems');
if (!Util.isEmpty(markerItems)) {
endx = markerItems[0].x;
endy = markerItems[0].y;
}
if (crossLineShapeY) { // 第一次进入时,画布需要单独绘制,所以需要先设定corss的位置
crossLineShapeY.move(endx, 0);
}
if (crossLineShapeX) {
crossLineShapeX.move(0, endy);
}
if (crosshairsRectShape) { // 绘制矩形辅助框,只在直角坐标系下生效
const isTransposed = this.get('isTransposed');
const items = this.get('items');
const firstItem = items[0];
const lastItem = items[items.length - 1];
const dim = isTransposed ? 'y' : 'x';
const attr = isTransposed ? 'height' : 'width';
let startDim = firstItem[dim];
if (items.length > 1 && firstItem[dim] > lastItem[dim]) {
startDim = lastItem[dim];
}
if (this.get('crosshairs').width) { // 用户定义了 width
crosshairsRectShape.attr(dim, startDim - this.get('crosshairs').width / 2);
crosshairsRectShape.attr(attr, this.get('crosshairs').width);
} else {
if (Util.isArray(firstItem.point[dim]) && !firstItem.size) { // 直方图
const width = firstItem.point[dim][1] - firstItem.point[dim][0];
crosshairsRectShape.attr(dim, firstItem.point[dim][0]);
crosshairsRectShape.attr(attr, width);
} else {
offset = (3 * firstItem.size) / 4;
crosshairsRectShape.attr(dim, startDim - offset);
if (items.length === 1) {
crosshairsRectShape.attr(attr, (3 * firstItem.size) / 2);
} else {
crosshairsRectShape.attr(attr, Math.abs(lastItem[dim] - firstItem[dim]) + 2 * offset);
}
}
}
}
const follow = this.get('follow');
container.style.left = follow ? (x + 'px') : 0;
container.style.top = follow ? (y + 'px') : 0;
}
// 设为可见
container.style.visibility = lastVisibility;
container.style.display = lastDisplay;
}
show() {
const crossLineShapeX = this.get('crossLineShapeX');
const crossLineShapeY = this.get('crossLineShapeY');
const crosshairsRectShape = this.get('crosshairsRectShape');
const markerGroup = this.get('markerGroup');
const container = this.get('container');
const canvas = this.get('canvas');
crossLineShapeX && crossLineShapeX.show();
crossLineShapeY && crossLineShapeY.show();
crosshairsRectShape && crosshairsRectShape.show();
markerGroup && markerGroup.show();
super.show();
container.style.visibility = 'visible';
// canvas.sort();
canvas.draw();
}
hide() {
const self = this;
const container = self.get('container');
if (container && container.style) {
const crossLineShapeX = self.get('crossLineShapeX');
const crossLineShapeY = self.get('crossLineShapeY');
const crosshairsRectShape = this.get('crosshairsRectShape');
const markerGroup = self.get('markerGroup');
const canvas = self.get('canvas');
container.style.visibility = 'hidden';
crossLineShapeX && crossLineShapeX.hide();
crossLineShapeY && crossLineShapeY.hide();
crosshairsRectShape && crosshairsRectShape.hide();
markerGroup && markerGroup.hide();
super.hide();
canvas.draw();
}
}
destroy() {
const self = this;
const crossLineShapeX = self.get('crossLineShapeX');
const crossLineShapeY = self.get('crossLineShapeY');
const markerGroup = self.get('markerGroup');
const crosshairsRectShape = self.get('crosshairsRectShape');
const container = self.get('container');
const containerTpl = self.get('containerTpl');
if (container && !(/^\#/.test(containerTpl))) {
container.parentNode.removeChild(container);
}
crossLineShapeX && crossLineShapeX.remove();
crossLineShapeY && crossLineShapeY.remove();
markerGroup && markerGroup.remove();
crosshairsRectShape && crosshairsRectShape.remove();
// super.remove();
super.destroy();
}
}
module.exports = Tooltip;