/** * @fileOverview The tooltip handler * @author sima.zhang */ const Util = require('../../util'); const { defaultColor } = require('../../global'); const FIELD_ORIGIN = '_origin'; function getScaleName(scale) { return scale.alias || scale.field; } const TooltipMixin = { _getIntervalSize(obj) { let size = null; const type = this.get('type'); const coord = this.get('coord'); if (coord.isRect && (type === 'interval' || type === 'schema')) { size = this.getSize(obj[FIELD_ORIGIN]); // 如果字段发生了映射,宽度计算就会报错 const dim = coord.isTransposed ? 'y' : 'x'; if (Util.isArray(obj[dim])) { const width = Math.abs(obj[dim][1] - obj[dim][0]); size = size < width ? null : size; // 直方图计算错误 } } return size; }, _snapEqual(v1, v2, scale) { let equals; v1 = scale.translate(v1); v2 = scale.translate(v2); if (scale.isCategory) { equals = v1 === v2; } else { equals = Util.snapEqual(v1, v2); } return equals; }, _getScaleValueByPoint(point) { let result = 0; const coord = this.get('coord'); const xScale = this.getXScale(); const invertPoint = coord.invert(point); let xValue = invertPoint.x; if (this.isInCircle() && xValue > (1 + xScale.rangeMax()) / 2) { xValue = xScale.rangeMin(); // 极坐标下,scale 的 range 被做过特殊处理 see view.js#L88 } result = xScale.invert(xValue); if (xScale.isCategory) { result = xScale.translate(result); // 防止分类类型 } return result; }, _getOriginByPoint(point) { const xScale = this.getXScale(); const yScale = this.getYScale(); const xField = xScale.field; const yField = yScale.field; const coord = this.get('coord'); const invertPoint = coord.invert(point); const xValue = xScale.invert(invertPoint.x); const yValue = yScale.invert(invertPoint.y); const result = {}; result[xField] = xValue; result[yField] = yValue; return result; }, _getScale(field) { const self = this; const scales = self.get('scales'); let rst = null; Util.each(scales, scale => { if (scale.field === field) { rst = scale; return false; } }); return rst; }, // 获取值对应的度量 _getTipValueScale() { const attrs = this.getAttrsForLegend(); let scale; Util.each(attrs, attr => { const tmpScale = attr.getScale(attr.type); if (tmpScale.isLinear) { // 如果指定字段是非position的,同时是连续的 scale = tmpScale; return false; } }); const xScale = this.getXScale(); const yScale = this.getYScale(); if (!scale && yScale && yScale.field === '..y') { return xScale; } return scale || yScale || xScale; }, _getTipTitleScale(titleField) { const self = this; if (titleField) { return self._getScale(titleField); } const position = self.getAttr('position'); const fields = position.getFields(); let tmpField; Util.each(fields, field => { if (!field.includes('..')) { tmpField = field; return false; } }); return self._getScale(tmpField); }, _filterValue(arr, point) { const coord = this.get('coord'); const yScale = this.getYScale(); const yField = yScale.field; const invertPoint = coord.invert(point); let yValue = invertPoint.y; yValue = yScale.invert(yValue); let rst = arr[arr.length - 1]; Util.each(arr, obj => { const origin = obj[FIELD_ORIGIN]; if ((origin[yField][0] <= yValue) && (origin[yField][1] >= yValue)) { rst = obj; return false; } }); return rst; }, getXDistance() { const self = this; let distance = self.get('xDistance'); if (!distance) { const xScale = self.getXScale(); if (xScale.isCategory) { distance = 1; } else { const values = xScale.values; // values 是无序的 let min = xScale.translate(values[0]); let max = min; Util.each(values, value => { // 时间类型需要 translate value = xScale.translate(value); if (value < min) { min = value; } if (value > max) { max = value; } }); const length = values.length; // 应该是除以 length - 1 distance = (max - min) / (length - 1); } self.set('xDistance', distance); } return distance; }, findPoint(point, dataArray) { const self = this; const type = self.get('type'); const xScale = self.getXScale(); const yScale = self.getYScale(); const xField = xScale.field; const yField = yScale.field; let rst = null; if (Util.indexOf([ 'heatmap', 'point' ], type) > -1) { const coord = self.get('coord'); const invertPoint = coord.invert(point); const xValue = xScale.invert(invertPoint.x); const yValue = yScale.invert(invertPoint.y); let min = Infinity; Util.each(dataArray, obj => { const distance = (obj[FIELD_ORIGIN][xField] - xValue) ** 2 + (obj[FIELD_ORIGIN][yField] - yValue) ** 2; if (distance < min) { min = distance; rst = obj; } }); return rst; } const first = dataArray[0]; let last = dataArray[dataArray.length - 1]; if (!first) { return rst; } const value = self._getScaleValueByPoint(point); // 根据该点获得对应度量后数据的值 const firstXValue = first[FIELD_ORIGIN][xField]; const firstYValue = first[FIELD_ORIGIN][yField]; const lastXValue = last[FIELD_ORIGIN][xField]; const isYRange = yScale.isLinear && Util.isArray(firstYValue); // 考虑 x 维度相同,y 是数组区间的情况 // 如果x的值是数组 if (Util.isArray(firstXValue)) { Util.each(dataArray, record => { const origin = record[FIELD_ORIGIN]; if (xScale.translate(origin[xField][0]) <= value && xScale.translate(origin[xField][1]) >= value) { if (isYRange) { if (!Util.isArray(rst)) { rst = []; } rst.push(record); } else { rst = record; return false; } } }); if (Util.isArray(rst)) { rst = this._filterValue(rst, point); } } else { let next; if (!xScale.isLinear && xScale.type !== 'timeCat') { Util.each(dataArray, (record, index) => { const origin = record[FIELD_ORIGIN]; if (self._snapEqual(origin[xField], value, xScale)) { if (isYRange) { if (!Util.isArray(rst)) { rst = []; } rst.push(record); } else { rst = record; return false; } } else if (xScale.translate(origin[xField]) <= value) { last = record; next = dataArray[index + 1]; } }); if (Util.isArray(rst)) { rst = this._filterValue(rst, point); } } else { if ((value > xScale.translate(lastXValue) || value < xScale.translate(firstXValue)) && (value > xScale.max || value < xScale.min)) { return null; } let firstIdx = 0; let lastIdx = dataArray.length - 1; let middleIdx; while (firstIdx <= lastIdx) { middleIdx = Math.floor((firstIdx + lastIdx) / 2); const item = dataArray[middleIdx][FIELD_ORIGIN][xField]; if (self._snapEqual(item, value, xScale)) { return dataArray[middleIdx]; } if (xScale.translate(item) <= xScale.translate(value)) { firstIdx = middleIdx + 1; last = dataArray[middleIdx]; next = dataArray[middleIdx + 1]; } else { if (lastIdx === 0) { last = dataArray[0]; } lastIdx = middleIdx - 1; } } } if (last && next) { // 计算最逼近的 if (Math.abs(xScale.translate(last[FIELD_ORIGIN][xField]) - value) > Math.abs(xScale.translate(next[FIELD_ORIGIN][xField]) - value)) { last = next; } } } const distance = self.getXDistance(); // 每个分类间的平均间距 if (!rst && Math.abs(xScale.translate(last[FIELD_ORIGIN][xField]) - value) <= distance / 2) { rst = last; } return rst; }, /** * @protected * 获取tooltip的标题 * @param {Object} origin 点的原始信息 * @param {String} titleField 标题的字段 * @return {String} 提示信息的标题 */ getTipTitle(origin, titleField) { let tipTitle = ''; const titleScale = this._getTipTitleScale(titleField); if (titleScale) { const value = origin[titleScale.field]; tipTitle = titleScale.getText(value); } else if (this.get('type') === 'heatmap') { // 热力图在不存在 title 的时候特殊处理 const xScale = this.getXScale(); const yScale = this.getYScale(); const xValue = xScale.getText(origin[xScale.field]); const yValue = yScale.getText(origin[yScale.field]); tipTitle = '( ' + xValue + ', ' + yValue + ' )'; } return tipTitle; }, getTipValue(origin, valueScale) { let value; const field = valueScale.field; const key = origin.key; value = origin[field]; if (Util.isArray(value)) { const tmp = []; Util.each(value, sub => { tmp.push(valueScale.getText(sub)); }); value = tmp.join('-'); } else { value = valueScale.getText(value, key); } return value; }, /** * @protected * 获取tooltip的名称 * @param {Object} origin 点的原始信息 * @return {String} 提示信息的名称 */ getTipName(origin) { let name; let nameScale; const groupScales = this._getGroupScales(); if (groupScales.length) { // 如果存在分组类型,取第一个分组类型 Util.each(groupScales, scale => { nameScale = scale; return false; }); } if (nameScale) { const field = nameScale.field; name = nameScale.getText(origin[field]); } else { const valueScale = this._getTipValueScale(); name = getScaleName(valueScale); } return name; }, /** * 获取点对应tooltip的信息 * @protected * @param {Object} point 原始的数据记录 * @param {String} titleField tooltipTitle 配置信息 * @return {Array} 一条或者多条记录 */ getTipItems(point, titleField) { const self = this; const origin = point[FIELD_ORIGIN]; const tipTitle = self.getTipTitle(origin, titleField); const tooltipCfg = self.get('tooltipCfg'); const items = []; let name; let value; function addItem(itemName, itemValue, cfg) { if (!Util.isNil(itemValue) && itemValue !== '') { // 值为null的时候,忽视 const item = { title: tipTitle, point, name: itemName || tipTitle, value: itemValue, color: point.color || defaultColor, marker: true }; item.size = self._getIntervalSize(point); items.push(Util.mix({}, item, cfg)); } } if (tooltipCfg) { const fields = tooltipCfg.fields; let cfg = tooltipCfg.cfg; const callbackParams = []; Util.each(fields, field => { callbackParams.push(origin[field]); }); if (cfg) { // 存在回调函数 if (Util.isFunction(cfg)) { cfg = cfg.apply(null, callbackParams); } const itemCfg = Util.mix({}, { point, title: tipTitle, color: point.color || defaultColor, marker: true // 默认展示 marker }, cfg); itemCfg.size = self._getIntervalSize(point); items.push(itemCfg); } else { Util.each(fields, field => { if (!Util.isNil(origin[field])) { // 字段数据为null ,undefined时不显示 const scale = self._getScale(field); name = getScaleName(scale); value = scale.getText(origin[field]); addItem(name, value); } }); } } else { const valueScale = self._getTipValueScale(); if (!Util.isNil(origin[valueScale.field])) { // 字段数据为null ,undefined时不显示 value = self.getTipValue(origin, valueScale); name = self.getTipName(origin); addItem(name, value); } } return items; }, isShareTooltip() { let shareTooltip = this.get('shareTooltip'); const type = this.get('type'); const view = this.get('view'); let options; if (view.get('parent')) { options = view.get('parent').get('options'); } else { options = view.get('options'); } if (type === 'interval') { const coord = this.get('coord'); const coordType = coord.type; if (coordType === 'theta' || (coordType === 'polar' && coord.isTransposed)) { shareTooltip = false; } } else if (!this.getYScale() || Util.inArray([ 'contour', 'point', 'polygon', 'edge' ], type)) { shareTooltip = false; } if (options.tooltip && Util.isBoolean(options.tooltip.shared)) { // 以用户设置的为准 shareTooltip = options.tooltip.shared; } return shareTooltip; } }; module.exports = TooltipMixin;