light-chart
Version:
Charts for mobile visualization.
565 lines (505 loc) • 15 kB
JavaScript
const Util = require('../util/common');
const Global = require('../global');
const Tooltip = require('../component/tooltip');
const Helper = require('../util/helper');
// Register the default configuration for Tooltip
Global.tooltip = Util.deepMix({
triggerOn: [ 'touchstart', 'touchmove' ],
// triggerOff: 'touchend',
alwaysShow: false,
showTitle: false,
showCrosshairs: false,
crosshairsStyle: {
stroke: 'rgba(0, 0, 0, 0.25)',
lineWidth: 1
},
showTooltipMarker: true,
background: {
radius: 1,
fill: 'rgba(0, 0, 0, 0.65)',
padding: [ 3, 5 ]
},
titleStyle: {
fontSize: 12,
fill: '#fff',
textAlign: 'start',
textBaseline: 'top'
},
nameStyle: {
fontSize: 12,
fill: 'rgba(255, 255, 255, 0.65)',
textAlign: 'start',
textBaseline: 'middle'
},
valueStyle: {
fontSize: 12,
fill: '#fff',
textAlign: 'start',
textBaseline: 'middle'
},
showItemMarker: true,
itemMarkerStyle: {
radius: 3,
symbol: 'circle',
lineWidth: 1,
stroke: '#fff'
},
layout: 'horizontal',
snap: false
}, Global.tooltip || {});
function _getTooltipValueScale(geom) {
const colorAttr = geom.getAttr('color');
if (colorAttr) {
const colorScale = colorAttr.getScale(colorAttr.type);
if (colorScale.isLinear) {
return colorScale;
}
}
const xScale = geom.getXScale();
const yScale = geom.getYScale();
if (yScale) {
return yScale;
}
return xScale;
}
function getTooltipName(geom, origin) {
let name;
let nameScale;
const groupScales = geom._getGroupScales();
if (groupScales.length) {
Util.each(groupScales, function(scale) {
nameScale = scale;
return false;
});
}
if (nameScale) {
const field = nameScale.field;
name = nameScale.getText(origin[field]);
} else {
const valueScale = _getTooltipValueScale(geom);
name = valueScale.alias || valueScale.field;
}
return name;
}
function getTooltipValue(geom, origin) {
const scale = _getTooltipValueScale(geom);
return scale.getText(origin[scale.field]);
}
function getTooltipTitle(geom, origin) {
const position = geom.getAttr('position');
const field = position.getFields()[0];
const scale = geom.get('scales')[field];
return scale.getText(origin[scale.field]);
}
function _indexOfArray(items, item) {
let rst = -1;
Util.each(items, function(sub, index) {
if (sub.title === item.title && sub.name === item.name && sub.value === item.value && sub.color === item.color) {
rst = index;
return false;
}
});
return rst;
}
function _uniqItems(items) {
const tmp = [];
Util.each(items, function(item) {
const index = _indexOfArray(tmp, item);
if (index === -1) {
tmp.push(item);
} else {
tmp[index] = item;
}
});
return tmp;
}
function isEqual(arr1, arr2) {
return JSON.stringify(arr1) === JSON.stringify(arr2);
}
class TooltipController {
constructor(cfg) {
this.enable = true;
this.cfg = {};
this.tooltip = null;
this.chart = null;
this.timeStamp = 0;
Util.mix(this, cfg);
const chart = this.chart;
this.canvasDom = chart.get('canvas').get('el');
}
_setCrosshairsCfg() {
const self = this;
const chart = self.chart;
const defaultCfg = Util.mix({}, Global.tooltip);
const geoms = chart.get('geoms');
const shapes = [];
Util.each(geoms, geom => {
const type = geom.get('type');
if (shapes.indexOf(type) === -1) {
shapes.push(type);
}
});
const coordType = chart.get('coord').type;
if (geoms.length && (coordType === 'cartesian' || coordType === 'rect')) {
if (shapes.length === 1 && [ 'line', 'area', 'path', 'point' ].indexOf(shapes[0]) !== -1) {
Util.mix(defaultCfg, {
showCrosshairs: true
});
}
}
return defaultCfg;
}
_getMaxLength(cfg = {}) {
const { layout, plotRange } = cfg;
return (layout === 'horizontal') ? plotRange.br.x - plotRange.bl.x : plotRange.bl.y - plotRange.tr.y;
}
render() {
const self = this;
if (self.tooltip) {
return;
}
const chart = self.chart;
const canvas = chart.get('canvas');
const frontPlot = chart.get('frontPlot').addGroup({
className: 'tooltipContainer',
zIndex: 10
});
const backPlot = chart.get('backPlot').addGroup({
className: 'tooltipContainer'
});
const plotRange = chart.get('plotRange');
const coord = chart.get('coord');
const defaultCfg = self._setCrosshairsCfg();
const cfg = self.cfg; // 通过 chart.tooltip() 接口传入的 tooltip 配置项
const tooltipCfg = Util.deepMix({
plotRange,
frontPlot,
backPlot,
canvas,
fixed: coord.transposed || coord.isPolar
}, defaultCfg, cfg); // 创建 tooltip 实例需要的配置,不应该修改 this.cfg,即用户传入的配置
tooltipCfg.maxLength = self._getMaxLength(tooltipCfg);
this._tooltipCfg = tooltipCfg;
const tooltip = new Tooltip(tooltipCfg);
self.tooltip = tooltip;
self.bindEvents();
}
clear() {
const tooltip = this.tooltip;
tooltip && tooltip.destroy();
this.tooltip = null;
this.prePoint = null;
this._lastActive = null;
this.unBindEvents();
}
_getTooltipMarkerStyle(cfg = {}) {
const { type, items } = cfg;
const tooltipCfg = this._tooltipCfg;
if (type === 'rect') {
let x;
let y;
let width;
let height;
const chart = this.chart;
const { tl, br } = chart.get('plotRange');
const coord = chart.get('coord');
const firstItem = items[0];
const lastItem = items[items.length - 1];
const intervalWidth = firstItem.width;
if (coord.transposed) {
x = tl.x;
y = lastItem.y - intervalWidth * 0.75;
width = br.x - tl.x;
height = firstItem.y - lastItem.y + 1.5 * intervalWidth;
} else {
x = firstItem.x - intervalWidth * 0.75;
y = tl.y;
width = lastItem.x - firstItem.x + 1.5 * intervalWidth;
height = br.y - tl.y;
}
cfg.style = Util.mix({
x,
y,
width,
height,
fill: '#CCD6EC',
opacity: 0.3
}, tooltipCfg.tooltipMarkerStyle);
} else {
cfg.style = Util.mix({
radius: 4,
fill: '#fff',
lineWidth: 2
}, tooltipCfg.tooltipMarkerStyle);
}
return cfg;
}
_setTooltip(point, items, tooltipMarkerCfg = {}) {
const lastActive = this._lastActive;
const tooltip = this.tooltip;
const cfg = this._tooltipCfg;
items = _uniqItems(items);
const chart = this.chart;
const coord = chart.get('coord');
const yScale = chart.getYScales()[0];
const snap = cfg.snap;
if (snap === false && yScale.isLinear) {
const invertPoint = coord.invertPoint(point);
const plot = chart.get('plotRange');
let tip;
let pos;
if (Helper.isPointInPlot(point, plot)) {
if (coord.transposed) {
tip = yScale.invert(invertPoint.x);
pos = point.x;
tooltip.setXTipContent(tip);
tooltip.setXTipPosition(pos);
tooltip.setYCrosshairPosition(pos);
} else {
tip = yScale.invert(invertPoint.y);
pos = point.y;
tooltip.setYTipContent(tip);
tooltip.setYTipPosition(pos);
tooltip.setXCrosshairPosition(pos);
}
}
}
if (cfg.onShow) {
cfg.onShow({
x: point.x,
y: point.y,
tooltip,
items,
tooltipMarkerCfg
});
}
if (isEqual(lastActive, items)) {
if (snap === false && (Util.directionEnabled(cfg.crosshairsType, 'y') || cfg.showYTip)) {
const canvas = this.chart.get('canvas');
canvas.draw();
}
return;
}
this._lastActive = items;
const onChange = cfg.onChange;
if (onChange) {
onChange({
x: point.x,
y: point.y,
tooltip,
items,
tooltipMarkerCfg
});
}
const first = items[0];
const title = first.title || first.name;
let xTipPosX = first.x;
if (items.length > 1) {
xTipPosX = (items[0].x + items[items.length - 1].x) / 2;
}
tooltip.setContent(title, items, coord.transposed);
tooltip.setPosition(items, point);
if (coord.transposed) {
let yTipPosY = first.y;
if (items.length > 1) {
yTipPosY = (items[0].y + items[items.length - 1].y) / 2;
}
tooltip.setYTipContent(title);
tooltip.setYTipPosition(yTipPosY);
tooltip.setXCrosshairPosition(yTipPosY);
if (snap) {
tooltip.setXTipContent(first.value);
tooltip.setXTipPosition(xTipPosX);
tooltip.setYCrosshairPosition(xTipPosX);
}
} else {
tooltip.setXTipContent(title);
tooltip.setXTipPosition(xTipPosX);
tooltip.setYCrosshairPosition(xTipPosX);
if (snap) {
tooltip.setYTipContent(first.value);
tooltip.setYTipPosition(first.y);
tooltip.setXCrosshairPosition(first.y);
}
}
const markerItems = tooltipMarkerCfg.items;
if (cfg.showTooltipMarker && markerItems.length) {
tooltipMarkerCfg = this._getTooltipMarkerStyle(tooltipMarkerCfg);
tooltip.setMarkers(tooltipMarkerCfg);
} else {
tooltip.clearMarkers();
}
tooltip.show();
}
showTooltip(point) {
const self = this;
const chart = self.chart;
let tooltipMarkerType;
const tooltipMarkerItems = [];
const items = [];
const cfg = self._tooltipCfg;
let marker;
if (cfg.showItemMarker) {
marker = cfg.itemMarkerStyle;
}
const geoms = chart.get('geoms');
const coord = chart.get('coord');
Util.each(geoms, geom => {
if (geom.get('visible')) {
const type = geom.get('type');
const records = geom.getSnapRecords(point);
Util.each(records, record => {
if (record.x && record.y) {
const { x, y, _origin, color } = record;
const tooltipItem = {
x,
y: Util.isArray(y) ? y[1] : y,
color: color || Global.defaultColor,
origin: _origin,
name: getTooltipName(geom, _origin),
value: getTooltipValue(geom, _origin),
title: getTooltipTitle(geom, _origin)
};
if (marker) {
tooltipItem.marker = Util.mix({
fill: color || Global.defaultColor
}, marker);
}
items.push(tooltipItem);
if ([ 'line', 'area', 'path' ].indexOf(type) !== -1) {
tooltipMarkerType = 'circle';
tooltipMarkerItems.push(tooltipItem);
} else if (type === 'interval' && (coord.type === 'cartesian' || coord.type === 'rect')) {
tooltipMarkerType = 'rect';
tooltipItem.width = geom.getSize(record._origin);
tooltipMarkerItems.push(tooltipItem);
}
}
});
}
});
if (items.length) {
const tooltipMarkerCfg = {
items: tooltipMarkerItems,
type: tooltipMarkerType
};
self._setTooltip(point, items, tooltipMarkerCfg);
} else {
self.hideTooltip();
}
}
hideTooltip() {
const cfg = this._tooltipCfg;
this._lastActive = null;
const tooltip = this.tooltip;
if (tooltip) {
tooltip.hide();
if (cfg.onHide) {
cfg.onHide({
tooltip
});
}
const canvas = this.chart.get('canvas');
canvas.draw();
}
}
handleShowEvent(ev) {
const chart = this.chart;
if (!this.enable || chart.get('_closeTooltip')) return;
const plot = chart.get('plotRange');
const point = Util.createEvent(ev, chart);
if (!Helper.isPointInPlot(point, plot) && !this._tooltipCfg.alwaysShow) { // not in chart plot
this.hideTooltip();
return;
}
const lastTimeStamp = this.timeStamp;
const timeStamp = +new Date();
if ((timeStamp - lastTimeStamp) > 16) {
this.showTooltip(point);
this.timeStamp = timeStamp;
}
}
handleHideEvent() {
const chart = this.chart;
if (!this.enable || chart.get('_closeTooltip')) return;
this.hideTooltip();
}
handleDocEvent(ev) {
const chart = this.chart;
if (!this.enable || chart.get('_closeTooltip')) return;
const canvasDom = this.canvasDom;
if (ev.target !== canvasDom) {
this.hideTooltip();
}
}
_handleEvent(methodName, method, action) {
const canvasDom = this.canvasDom;
Util.each([].concat(methodName), aMethod => {
if (action === 'bind') {
Util.addEventListener(canvasDom, aMethod, method);
} else {
Util.removeEventListener(canvasDom, aMethod, method);
}
});
}
bindEvents() {
const cfg = this._tooltipCfg;
const { triggerOn, triggerOff, alwaysShow } = cfg;
const showMethod = Util.wrapBehavior(this, 'handleShowEvent');
const hideMethod = Util.wrapBehavior(this, 'handleHideEvent');
triggerOn && this._handleEvent(triggerOn, showMethod, 'bind');
triggerOff && this._handleEvent(triggerOff, hideMethod, 'bind');
// TODO: 当用户点击 canvas 外的事件时 tooltip 消失
if (!alwaysShow) {
const docMethod = Util.wrapBehavior(this, 'handleDocEvent');
Util.isBrowser && Util.addEventListener(document, 'touchstart', docMethod);
}
}
unBindEvents() {
const cfg = this._tooltipCfg;
const { triggerOn, triggerOff, alwaysShow } = cfg;
const showMethod = Util.getWrapBehavior(this, 'handleShowEvent');
const hideMethod = Util.getWrapBehavior(this, 'handleHideEvent');
triggerOn && this._handleEvent(triggerOn, showMethod, 'unBind');
triggerOff && this._handleEvent(triggerOff, hideMethod, 'unBind');
if (!alwaysShow) {
const docMethod = Util.getWrapBehavior(this, 'handleDocEvent');
Util.isBrowser && Util.removeEventListener(document, 'touchstart', docMethod);
}
}
}
module.exports = {
init(chart) {
const tooltipController = new TooltipController({
chart
});
chart.set('tooltipController', tooltipController);
chart.tooltip = function(enable, cfg) {
if (Util.isObject(enable)) {
cfg = enable;
enable = true;
}
tooltipController.enable = enable;
if (cfg) {
tooltipController.cfg = cfg;
}
return this;
};
},
afterGeomDraw(chart) {
const tooltipController = chart.get('tooltipController');
tooltipController.render();
chart.showTooltip = function(point) {
tooltipController.showTooltip(point);
return this;
};
chart.hideTooltip = function() {
tooltipController.hideTooltip();
return this;
};
},
clearInner(chart) {
const tooltipController = chart.get('tooltipController');
tooltipController.clear();
}
};