UNPKG

@antv/g2

Version:

the Grammar of Graphics in Javascript

365 lines (326 loc) 11.3 kB
import { contains, filter, find, hasKey, isArray, isNil, isNumberEqual, isObject, memoize, values } from '@antv/util'; import { FIELD_ORIGIN, GROUP_ATTRS } from '../constant'; import { Attribute, Scale } from '../dependents'; import Geometry from '../geometry/base'; import { Data, Datum, MappingDatum, Point } from '../interface'; import { getName } from './scale'; function snapEqual(v1: any, v2: any, scale: Scale) { const value1 = scale.translate(v1); const value2 = scale.translate(v2); return isNumberEqual(value1, value2); } function getXValueByPoint(point: Point, geometry: Geometry): number { const coordinate = geometry.coordinate; const xScale = geometry.getXScale(); const range = xScale.range; const rangeMax = range[range.length - 1]; const rangeMin = range[0]; const invertPoint = coordinate.invert(point); let xValue = invertPoint.x; if (coordinate.isPolar && xValue > (1 + rangeMax) / 2) { xValue = rangeMin; // 极坐标下,scale 的 range 被做过特殊处理 } return xScale.translate(xScale.invert(xValue)); } function filterYValue(data: Data, point: Point, geometry: Geometry) { const coordinate = geometry.coordinate; const yScale = geometry.getYScale(); const yField = yScale.field; const invertPoint = coordinate.invert(point); const yValue = yScale.invert(invertPoint.y); const result = find(data, (obj: Datum) => { const originData = obj[FIELD_ORIGIN]; return originData[yField][0] <= yValue && originData[yField][1] >= yValue; }); return result || data[data.length - 1]; } const getXDistance = memoize((scale: Scale) => { if (scale.isCategory) { return 1; } const scaleValues = scale.values; // values 是无序的 const length = scaleValues.length; let min = scale.translate(scaleValues[0]); let max = min; for (let index = 0; index < length; index++) { const value = scaleValues[index]; // 时间类型需要 translate const numericValue = scale.translate(value); if (numericValue < min) { min = numericValue; } if (numericValue > max) { max = numericValue; } } return (max - min) / (length - 1); }); function getTooltipTitle(originData: Datum, geometry: Geometry, title: string) { let titleField = title; if (!title) { const positionAttr = geometry.getAttribute('position'); const fields = positionAttr.getFields(); titleField = fields[0]; } const scales = geometry.scales; if (scales[titleField]) { // 如果创建了该字段对应的 scale,则通过 scale.getText() 方式取值,因为用户可能对数据进行了格式化 return scales[titleField].getText(originData[titleField]); } // 如果没有对应的 scale,则从原始数据中取值,如果原始数据中仍不存在,则直接放回 title 值 return hasKey(originData, titleField) ? originData[titleField] : titleField; } function getAttributesForLegend(geometry: Geometry) { const attributes = values(geometry.attributes); return filter(attributes, (attribute: Attribute) => contains(GROUP_ATTRS, attribute.type)); } function getTooltipValueScale(geometry: Geometry) { const attributes = getAttributesForLegend(geometry); let scale; for (const attribute of attributes) { const tmpScale = attribute.getScale(attribute.type); if (tmpScale && tmpScale.isLinear) { // 如果指定字段是非 position 的,同时是连续的 scale = tmpScale; break; } } const xScale = geometry.getXScale(); const yScale = geometry.getYScale(); return scale || yScale || xScale; } function getTooltipValue(originData: Datum, valueScale: Scale) { const field = valueScale.field; const value = originData[field]; if (isArray(value)) { const texts = value.map((eachValue) => { return valueScale.getText(eachValue); }); return texts.join('-'); } return valueScale.getText(value); } // 根据原始数据获取 tooltip item 中 name 值 function getTooltipName(originData: Datum, geometry: Geometry) { let nameScale: Scale; const groupScales = geometry.getGroupScales(); if (groupScales.length) { // 如果存在分组类型,取第一个分组类型 nameScale = groupScales[0]; } if (nameScale) { const field = nameScale.field; return nameScale.getText(originData[field]); } const valueScale = getTooltipValueScale(geometry); return getName(valueScale); } /** * @ignore * Finds data from geometry by point * @param point canvas point * @param data an item of geometry.dataArray * @param geometry * @returns */ export function findDataByPoint(point: Point, data: MappingDatum[], geometry: Geometry) { if (data.length === 0) { return null; } const geometryType = geometry.type; const xScale = geometry.getXScale(); const yScale = geometry.getYScale(); const xField = xScale.field; const yField = yScale.field; let rst = null; // 热力图采用最小逼近策略查找 point 击中的数据 if (geometryType === 'heatmap' || geometryType === 'point') { // 将 point 画布坐标转换为原始数据值 const coordinate = geometry.coordinate; const invertPoint = coordinate.invert(point); // 转换成归一化的数据 const x = xScale.invert(invertPoint.x); // 转换为原始值 const y = yScale.invert(invertPoint.y); // 转换为原始值 let min = Infinity; for (let index = 0; index < data.length; index++) { const obj = data[index]; const originData = obj[FIELD_ORIGIN]; const range = (originData[xField] - x) ** 2 + (originData[yField] - y) ** 2; if (range < min) { min = range; rst = obj; } } return rst; } // 其他 Geometry 类型按照 x 字段数据进行查找 const first = data[0]; let last = data[data.length - 1]; const xValue = getXValueByPoint(point, geometry); const firstXValue = first[FIELD_ORIGIN][xField]; const firstYValue = first[FIELD_ORIGIN][yField]; const lastXValue = last[FIELD_ORIGIN][xField]; const isYArray = yScale.isLinear && isArray(firstYValue); // 考虑 x 维度相同,y 是数组区间的情况 // 如果 x 的值是数组 if (isArray(firstXValue)) { for (let index = 0; index < data.length; index++) { const record = data[index]; const originData = record[FIELD_ORIGIN]; // xValue 在 originData[xField] 的数值区间内 if (xScale.translate(originData[xField][0]) <= xValue && xScale.translate(originData[xField][1]) >= xValue) { if (isYArray) { // 层叠直方图场景,x 和 y 都是数组区间 if (!isArray(rst)) { rst = []; } rst.push(record); } else { rst = record; break; } } } if (isArray(rst)) { rst = filterYValue(rst, point, geometry); } } else { let next; if (!xScale.isLinear && xScale.type !== 'timeCat') { // x 轴对应的数据为非线性以及非时间类型的数据采用遍历查找 for (let index = 0; index < data.length; index++) { const record = data[index]; const originData = record[FIELD_ORIGIN]; if (snapEqual(originData[xField], xValue, xScale)) { if (isYArray) { if (!isArray(rst)) { rst = []; } rst.push(record); } else { rst = record; break; } } else if (xScale.translate(originData[xField]) <= xValue) { last = record; next = data[index + 1]; } } if (isArray(rst)) { rst = filterYValue(rst, point, geometry); } } else { // x 轴对应的数据为线性以及时间类型,进行二分查找,性能更好 if ( (xValue > xScale.translate(lastXValue) || xValue < xScale.translate(firstXValue)) && (xValue > xScale.max || xValue < xScale.min) ) { // 不在数据范围内 return null; } let firstIdx = 0; let lastIdx = data.length - 1; let middleIdx; while (firstIdx <= lastIdx) { middleIdx = Math.floor((firstIdx + lastIdx) / 2); const item = data[middleIdx][FIELD_ORIGIN][xField]; if (snapEqual(item, xValue, xScale)) { return data[middleIdx]; } if (xScale.translate(item) <= xScale.translate(xValue)) { firstIdx = middleIdx + 1; last = data[middleIdx]; next = data[middleIdx + 1]; } else { if (lastIdx === 0) { last = data[0]; } lastIdx = middleIdx - 1; } } } if (last && next) { // 计算最逼近的 if ( Math.abs(xScale.translate(last[FIELD_ORIGIN][xField]) - xValue) > Math.abs(xScale.translate(next[FIELD_ORIGIN][xField]) - xValue) ) { last = next; } } } const distance = getXDistance(geometry.getXScale()); // 每个分类间的平均间距 if (!rst && Math.abs(xScale.translate(last[FIELD_ORIGIN][xField]) - xValue) <= distance / 2) { rst = last; } return rst; } /** * @ignore * Gets tooltip items * @param data * @param geometry * @param [title] * @returns */ export function getTooltipItems(data: MappingDatum, geometry: Geometry, title: string = '') { const originData = data[FIELD_ORIGIN]; const tooltipTitle = getTooltipTitle(originData, geometry, title); const tooltipOption = geometry.tooltipOption; const { defaultColor } = geometry.theme; const items = []; let name; let value; function addItem(itemName, itemValue) { if (!isNil(itemValue) && itemValue !== '') { // 值为 null的时候,忽视 const item = { title: tooltipTitle, data: originData, // 原始数据 mappingData: data, // 映射后的数据 name: itemName, value: itemValue, color: data.color || defaultColor, marker: true, }; items.push(item); } } if (isObject(tooltipOption)) { const { fields, callback } = tooltipOption; if (callback) { // 用户定义了回调函数 const callbackParams = fields.map((field: string) => { return data[FIELD_ORIGIN][field]; }); const cfg = callback(...callbackParams); const itemCfg = { data: data[FIELD_ORIGIN], // 原始数据 mappingData: data, // 映射后的数据 title: tooltipTitle, color: data.color || defaultColor, marker: true, // 默认展示 marker ...cfg, }; items.push(itemCfg); } else { const scales = geometry.scales; for (const field of fields) { if (!isNil(originData[field])) { // 字段数据为null, undefined 时不显示 const scale = scales[field]; name = getName(scale); value = scale.getText(originData[field]); addItem(name, value); } } } } else { const valueScale = getTooltipValueScale(geometry); if (!isNil(originData[valueScale.field])) { // 字段数据为null ,undefined时不显示 value = getTooltipValue(originData, valueScale); name = getTooltipName(originData, geometry); addItem(name, value); } } return items; }