/** * @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;