@antv/g2
Version:
the Grammar of Graphics in Javascript
893 lines • 37 kB
JavaScript
"use strict";
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Tooltip = exports.tooltip = exports.seriesTooltip = void 0;
const g_1 = require("@antv/g");
const d3_array_1 = require("d3-array");
const util_1 = require("@antv/util");
const component_1 = require("@antv/component");
const scale_1 = require("@antv/scale");
const helper_1 = require("../utils/helper");
const coordinate_1 = require("../utils/coordinate");
const vector_1 = require("../utils/vector");
const scale_2 = require("../utils/scale");
const utils_1 = require("./utils");
const event_1 = require("./event");
function getContainer(group, mount) {
if (mount) {
return typeof mount === 'string' ? document.querySelector(mount) : mount;
}
const canvas = group.ownerDocument.defaultView
.getContextService()
.getDomElement();
return canvas.parentElement;
}
function getBounding(root) {
const bbox = root.getRenderBounds();
const { min: [x1, y1], max: [x2, y2], } = bbox;
return {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
};
}
function getContainerOffset(container1, container2) {
const r1 = container1.getBoundingClientRect();
const r2 = container2.getBoundingClientRect();
return {
x: r1.x - r2.x,
y: r1.y - r2.y,
};
}
function createTooltip(container, x0, y0, position, enterable, bounding, containerOffset, css = {}, offset = [10, 10]) {
const defaults = {
'.g2-tooltip': {},
'.g2-tooltip-title': {
overflow: 'hidden',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
},
};
const tooltipElement = new component_1.Tooltip({
className: 'tooltip',
style: {
x: x0,
y: y0,
container: containerOffset,
data: [],
bounding,
position,
enterable,
title: '',
offset,
template: {
prefixCls: 'g2-',
},
style: (0, util_1.deepMix)(defaults, css),
},
});
container.appendChild(tooltipElement.HTMLTooltipElement);
return tooltipElement;
}
function showTooltip({ root, data, x, y, render, event, single, position = 'right-bottom', enterable = false, css, mount, bounding, offset, }) {
const container = getContainer(root, mount);
const canvasContainer = getContainer(root);
// All the views share the same tooltip.
const parent = single ? canvasContainer : root;
const b = bounding || getBounding(root);
const containerOffset = getContainerOffset(canvasContainer, container);
const { tooltipElement = createTooltip(container, x, y, position, enterable, b, containerOffset, css, offset), } = parent;
const { items, title = '' } = data;
tooltipElement.update(Object.assign({ x,
y, data: items, title,
position,
enterable }, (render !== undefined && {
content: render(event, { items, title }),
})));
parent.tooltipElement = tooltipElement;
}
function hideTooltip({ root, single, emitter, nativeEvent = true, event = null, }) {
if (nativeEvent) {
emitter.emit('tooltip:hide', { nativeEvent });
}
const container = getContainer(root);
const parent = single ? container : root;
const { tooltipElement } = parent;
if (tooltipElement) {
// Must be clientX, clientY.
tooltipElement.hide(event === null || event === void 0 ? void 0 : event.clientX, event === null || event === void 0 ? void 0 : event.clientY);
}
hideRuleY(root);
hideRuleX(root);
hideMarker(root);
}
function destroyTooltip({ root, single }) {
const container = getContainer(root);
const parent = single ? container : root;
if (!parent)
return;
const { tooltipElement } = parent;
if (tooltipElement) {
tooltipElement.destroy();
parent.tooltipElement = undefined;
}
hideRuleY(root);
hideRuleX(root);
hideMarker(root);
}
function showUndefined(item) {
const { value } = item;
return Object.assign(Object.assign({}, item), { value: value === undefined ? 'undefined' : value });
}
function singleItem(element) {
const { __data__: datum } = element;
const { title, items = [] } = datum;
const newItems = items
.filter(helper_1.defined)
.map((_a) => {
var { color = itemColorOf(element) } = _a, item = __rest(_a, ["color"]);
return (Object.assign(Object.assign({}, item), { color }));
})
.map(showUndefined);
return Object.assign(Object.assign({}, (title && { title })), { items: newItems });
}
function groupNameOf(scale, datum) {
const { color: scaleColor, series: scaleSeries, facet = false } = scale;
const { color, series } = datum;
const invertAble = (scale) => {
return (scale &&
scale.invert &&
!(scale instanceof scale_1.Band) &&
!(scale instanceof scale_1.Constant));
};
// For non constant color channel.
if (invertAble(scaleSeries)) {
const cloned = scaleSeries.clone();
return cloned.invert(series);
}
if (series &&
scaleSeries instanceof scale_1.Band &&
scaleSeries.invert(series) !== color &&
!facet) {
return scaleSeries.invert(series);
}
if (invertAble(scaleColor)) {
const name = scaleColor.invert(color);
// For threshold scale.
if (Array.isArray(name))
return null;
return name;
}
return null;
}
function itemColorOf(element) {
const fill = element.getAttribute('fill');
const stroke = element.getAttribute('stroke');
const { __data__: datum } = element;
const { color = fill && fill !== 'transparent' ? fill : stroke } = datum;
return color;
}
function unique(items, key = (d) => d) {
const valueName = new Map(items.map((d) => [key(d), d]));
return Array.from(valueName.values());
}
function groupItems(elements, scale, groupName, data = elements.map((d) => d['__data__']), theme = {}) {
const key = (d) => (d instanceof Date ? +d : d);
const T = unique(data.map((d) => d.title), key).filter(helper_1.defined);
const newItems = data
.flatMap((datum, i) => {
const element = elements[i];
const { items = [], title } = datum;
const definedItems = items.filter(helper_1.defined);
// If there is only one item, use groupName as title by default.
const useGroupName = groupName !== undefined ? groupName : items.length <= 1 ? true : false;
return definedItems.map((_a) => {
var { color = itemColorOf(element) || theme.color, name } = _a, item = __rest(_a, ["color", "name"]);
const groupName = groupNameOf(scale, datum);
const name1 = useGroupName ? groupName || name : name || groupName;
return Object.assign(Object.assign({}, item), { color, name: name1 || title });
});
})
.map(showUndefined);
return Object.assign(Object.assign({}, (T.length > 0 && { title: T.join(',') })), { items: unique(newItems, (d) => `(${key(d.name)}, ${key(d.value)}, ${key(d.color)})`) });
}
function updateRuleX(root, points, mouse, _a) {
var { plotWidth, plotHeight, mainWidth, mainHeight, startX, startY, transposed, polar, insetLeft, insetTop } = _a, rest = __rest(_a, ["plotWidth", "plotHeight", "mainWidth", "mainHeight", "startX", "startY", "transposed", "polar", "insetLeft", "insetTop"]);
const defaults = Object.assign({ lineWidth: 1, stroke: '#1b1e23', strokeOpacity: 0.5 }, rest);
const createCircle = (cx, cy, r) => {
const circle = new g_1.Circle({
style: Object.assign({ cx,
cy,
r }, defaults),
});
root.appendChild(circle);
return circle;
};
const createLine = (x1, x2, y1, y2) => {
const line = new g_1.Line({
style: Object.assign({ x1,
x2,
y1,
y2 }, defaults),
});
root.appendChild(line);
return line;
};
const minDistPoint = (mouse, points) => {
// only one point do not need compute
if (points.length === 1) {
return points[0];
}
const dists = points.map((p) => (0, vector_1.dist)(p, mouse));
const minDistIndex = (0, d3_array_1.minIndex)(dists, (d) => d);
return points[minDistIndex];
};
const target = minDistPoint(mouse, points);
const pointsOf = () => {
if (transposed)
return [
startX + target[0],
startX + target[0],
startY,
startY + plotHeight,
];
return [startX, startX + plotWidth, target[1] + startY, target[1] + startY];
};
const pointsOfPolar = () => {
const cx = startX + insetLeft + mainWidth / 2;
const cy = startY + insetTop + mainHeight / 2;
const cdist = (0, vector_1.dist)([cx, cy], target);
return [cx, cy, cdist];
};
if (polar) {
const [cx, cy, r] = pointsOfPolar();
const ruleX = root.ruleX || createCircle(cx, cy, r);
ruleX.style.cx = cx;
ruleX.style.cy = cy;
ruleX.style.r = r;
root.ruleX = ruleX;
}
else {
const [x1, x2, y1, y2] = pointsOf();
const ruleX = root.ruleX || createLine(x1, x2, y1, y2);
ruleX.style.x1 = x1;
ruleX.style.x2 = x2;
ruleX.style.y1 = y1;
ruleX.style.y2 = y2;
root.ruleX = ruleX;
}
}
function updateRuleY(root, points, _a) {
var { plotWidth, plotHeight, mainWidth, mainHeight, startX, startY, transposed, polar, insetLeft, insetTop } = _a, rest = __rest(_a, ["plotWidth", "plotHeight", "mainWidth", "mainHeight", "startX", "startY", "transposed", "polar", "insetLeft", "insetTop"]);
const defaults = Object.assign({ lineWidth: 1, stroke: '#1b1e23', strokeOpacity: 0.5 }, rest);
const Y = points.map((p) => p[1]);
const X = points.map((p) => p[0]);
const y = (0, d3_array_1.mean)(Y);
const x = (0, d3_array_1.mean)(X);
const pointsOf = () => {
if (polar) {
const r = Math.min(mainWidth, mainHeight) / 2;
const cx = startX + insetLeft + mainWidth / 2;
const cy = startY + insetTop + mainHeight / 2;
const a = (0, vector_1.angle)((0, vector_1.sub)([x, y], [cx, cy]));
const x0 = cx + r * Math.cos(a);
const y0 = cy + r * Math.sin(a);
return [cx, x0, cy, y0];
}
if (transposed)
return [startX, startX + plotWidth, y + startY, y + startY];
return [x + startX, x + startX, startY, startY + plotHeight];
};
const [x1, x2, y1, y2] = pointsOf();
const createLine = () => {
const line = new g_1.Line({
style: Object.assign({ x1,
x2,
y1,
y2 }, defaults),
});
root.appendChild(line);
return line;
};
// Only update rule with defined series elements.
if (X.length > 0) {
const ruleY = root.ruleY || createLine();
ruleY.style.x1 = x1;
ruleY.style.x2 = x2;
ruleY.style.y1 = y1;
ruleY.style.y2 = y2;
root.ruleY = ruleY;
}
}
function hideRuleY(root) {
if (root.ruleY) {
root.ruleY.remove();
root.ruleY = undefined;
}
}
function hideRuleX(root) {
if (root.ruleX) {
root.ruleX.remove();
root.ruleX = undefined;
}
}
function updateMarker(root, { data, style, theme }) {
if (root.markers)
root.markers.forEach((d) => d.remove());
const { type = '' } = style;
const markers = data
.filter((d) => {
const [{ x, y }] = d;
return (0, helper_1.defined)(x) && (0, helper_1.defined)(y);
})
.map((d) => {
const [{ color, element }, point] = d;
const originColor = color || // encode value
element.style.fill ||
element.style.stroke ||
theme.color;
const fill = type === 'hollow' ? 'transparent' : originColor;
const stroke = type === 'hollow' ? originColor : '#fff';
const shape = new g_1.Circle({
style: Object.assign({ cx: point[0], cy: point[1], fill, r: 4, stroke, lineWidth: 2 }, style),
});
return shape;
});
for (const marker of markers)
root.appendChild(marker);
root.markers = markers;
}
function hideMarker(root) {
if (root.markers) {
root.markers.forEach((d) => d.remove());
root.markers = [];
}
}
function interactionKeyof(markState, key) {
return Array.from(markState.values()).some(
// @ts-ignore
(d) => { var _a; return (_a = d.interaction) === null || _a === void 0 ? void 0 : _a[key]; });
}
function maybeValue(specified, defaults) {
return specified === undefined ? defaults : specified;
}
function isEmptyTooltipData(data) {
const { title, items } = data;
if (items.length === 0 && title === undefined)
return true;
return false;
}
function hasSeries(markState) {
return Array.from(markState.values()).some(
// @ts-ignore
(d) => { var _a; return ((_a = d.interaction) === null || _a === void 0 ? void 0 : _a.seriesTooltip) && d.tooltip; });
}
/**
* Show tooltip for series item.
*/
function seriesTooltip(root, _a) {
var { elements: elementsof, sort: sortFunction, filter: filterFunction, scale, coordinate, crosshairs, crosshairsX, crosshairsY, render, groupName, emitter, wait = 50, leading = true, trailing = false, startX = 0, startY = 0, body = true, single = true, position, enterable, mount, bounding, theme, offset, disableNative = false, marker = true, preserve = false, style: _style = {}, css = {} } = _a, rest = __rest(_a, ["elements", "sort", "filter", "scale", "coordinate", "crosshairs", "crosshairsX", "crosshairsY", "render", "groupName", "emitter", "wait", "leading", "trailing", "startX", "startY", "body", "single", "position", "enterable", "mount", "bounding", "theme", "offset", "disableNative", "marker", "preserve", "style", "css"]);
const elements = elementsof(root);
const transposed = (0, coordinate_1.isTranspose)(coordinate);
const polar = (0, coordinate_1.isPolar)(coordinate);
const style = (0, util_1.deepMix)(_style, rest);
const { innerWidth: plotWidth, innerHeight: plotHeight, width: mainWidth, height: mainHeight, insetLeft, insetTop, } = coordinate.getOptions();
// Split elements into series elements and item elements.
const seriesElements = [];
const itemElements = [];
for (const element of elements) {
const { __data__: data } = element;
const { seriesX, title, items } = data;
if (seriesX)
seriesElements.push(element);
else if (title || items)
itemElements.push(element);
}
const inInterval = (d) => d.markType === 'interval';
const isBar = itemElements.length &&
itemElements.every(inInterval) &&
!(0, coordinate_1.isPolar)(coordinate);
const xof = (d) => d.__data__.x;
// For band scale x, find the closest series element to focus,
// useful for interval + line mark.
const isBandScale = !!scale.x.getBandWidth;
const closest = isBandScale && itemElements.length > 0;
// Sorted elements from top to bottom visually,
// or from right to left in transpose coordinate.
seriesElements.sort((a, b) => {
const index = transposed ? 0 : 1;
const minY = (d) => d.getBounds().min[index];
return transposed ? minY(b) - minY(a) : minY(a) - minY(b);
});
const extent = (d) => {
const index = transposed ? 1 : 0;
const { min, max } = d.getLocalBounds();
return (0, d3_array_1.sort)([min[index], max[index]]);
};
// Sort itemElements for bisector search.
if (isBar)
elements.sort((a, b) => xof(a) - xof(b));
else {
itemElements.sort((a, b) => {
const [minA, maxA] = extent(a);
const [minB, maxB] = extent(b);
const midA = (minA + maxA) / 2;
const midB = (minB + maxB) / 2;
return transposed ? midB - midA : midA - midB;
});
}
// Get sortedIndex and X for each series elements
const elementSortedX = new Map(seriesElements.map((element) => {
const { __data__: data } = element;
const { seriesX } = data;
const seriesIndex = seriesX.map((_, i) => i);
const sortedIndex = (0, d3_array_1.sort)(seriesIndex, (i) => seriesX[+i]);
return [element, [sortedIndex, seriesX]];
}));
const { x: scaleX } = scale;
// Apply offset for band scale x.
const offsetX = (scaleX === null || scaleX === void 0 ? void 0 : scaleX.getBandWidth) ? scaleX.getBandWidth() / 2 : 0;
const abstractX = (focus) => {
const [normalizedX] = coordinate.invert(focus);
return normalizedX - offsetX;
};
const indexByFocus = (event, focus, I, X) => {
// _x is from emit event, to find the right element.
const { _x } = event;
const finalX = _x !== undefined ? scaleX.map(_x) : abstractX(focus);
const DX = X.filter(helper_1.defined);
const [minX, maxX] = (0, d3_array_1.sort)([DX[0], DX[DX.length - 1]]);
// If only has one element(minX == maxX), show tooltip when hover whole chart
const isOnlyOneElement = minX === maxX;
// If closest is true, always find at least one element.
// Otherwise, skip element out of plot area.
if (!closest && (finalX < minX || finalX > maxX) && !isOnlyOneElement)
return null;
const search = (0, d3_array_1.bisector)((i) => X[+i]).center;
const i = search(I, finalX);
return I[i];
};
const elementsByFocus = isBar
? (focus, elements) => {
const search = (0, d3_array_1.bisector)(xof).center;
const i = search(elements, abstractX(focus));
const find = elements[i];
const groups = (0, d3_array_1.group)(elements, xof);
const selected = groups.get(xof(find));
return selected;
}
: (focus, elements) => {
const index = transposed ? 1 : 0;
const x = focus[index];
const filtered = elements.filter((element) => {
const [min, max] = extent(element);
return x >= min && x <= max;
});
// If closet is true, always find at least one element.
if (!closest || filtered.length > 0)
return filtered;
// Search the closet element to the focus.
const search = (0, d3_array_1.bisector)((element) => {
const [min, max] = extent(element);
return (min + max) / 2;
}).center;
const i = search(elements, x);
return [elements[i]].filter(helper_1.defined);
};
const seriesData = (element, index) => {
const { __data__: data } = element;
return Object.fromEntries(Object.entries(data)
.filter(([key]) => key.startsWith('series') && key !== 'series')
.map(([key, V]) => {
const d = V[index];
return [(0, util_1.lowerFirst)(key.replace('series', '')), d];
}));
};
const update = (0, util_1.throttle)((event) => {
var _a;
const mouse = (0, utils_1.mousePosition)(root, event);
if (!mouse)
return;
const bbox = (0, utils_1.bboxOf)(root);
const x = bbox.min[0];
const y = bbox.min[1];
const focus = [mouse[0] - startX, mouse[1] - startY];
if (!focus)
return;
// Get selected item element.
const selectedItems = elementsByFocus(focus, itemElements);
// Get selected data item from both series element and item element.
const selectedSeriesElements = [];
const selectedSeriesData = [];
for (const element of seriesElements) {
const [sortedIndex, X] = elementSortedX.get(element);
const index = indexByFocus(event, focus, sortedIndex, X);
if (index !== null) {
selectedSeriesElements.push(element);
const d = seriesData(element, index);
const { x, y } = d;
const p = coordinate.map([(x || 0) + offsetX, y || 0]);
selectedSeriesData.push([Object.assign(Object.assign({}, d), { element }), p]);
}
}
// Filter selectedSeriesData with different x,
// make sure there is only one x closest to focusX.
const SX = Array.from(new Set(selectedSeriesData.map((d) => d[0].x)));
const closestX = SX[(0, d3_array_1.minIndex)(SX, (x) => Math.abs(x - abstractX(focus)))];
const filteredSeriesData = selectedSeriesData.filter((d) => d[0].x === closestX);
const selectedData = [
...filteredSeriesData.map((d) => d[0]),
...selectedItems.map((d) => d.__data__),
];
// Get the displayed tooltip data.
const selectedElements = [...selectedSeriesElements, ...selectedItems];
const tooltipData = groupItems(selectedElements, scale, groupName, selectedData, theme);
// Sort items and filter items.
if (sortFunction) {
tooltipData.items.sort((a, b) => sortFunction(a) - sortFunction(b));
}
if (filterFunction) {
tooltipData.items = tooltipData.items.filter(filterFunction);
}
// Hide tooltip with no selected tooltip.
if (selectedElements.length === 0 || isEmptyTooltipData(tooltipData)) {
hide(event);
return;
}
if (body) {
showTooltip({
root,
data: tooltipData,
x: mouse[0] + x,
y: mouse[1] + y,
render,
event,
single,
position,
enterable,
mount,
bounding,
css,
offset,
});
}
if (crosshairs || crosshairsX || crosshairsY) {
const ruleStyle = (0, helper_1.subObject)(style, 'crosshairs');
const ruleStyleX = Object.assign(Object.assign({}, ruleStyle), (0, helper_1.subObject)(style, 'crosshairsX'));
const ruleStyleY = Object.assign(Object.assign({}, ruleStyle), (0, helper_1.subObject)(style, 'crosshairsY'));
const points = filteredSeriesData.map((d) => d[1]);
if (crosshairsX) {
updateRuleX(root, points, mouse, Object.assign(Object.assign({}, ruleStyleX), { plotWidth,
plotHeight,
mainWidth,
mainHeight,
insetLeft,
insetTop,
startX,
startY,
transposed,
polar }));
}
if (crosshairsY) {
updateRuleY(root, points, Object.assign(Object.assign({}, ruleStyleY), { plotWidth,
plotHeight,
mainWidth,
mainHeight,
insetLeft,
insetTop,
startX,
startY,
transposed,
polar }));
}
}
if (marker) {
const markerStyles = (0, helper_1.subObject)(style, 'marker');
updateMarker(root, {
data: filteredSeriesData,
style: markerStyles,
theme,
});
}
// X in focus may related multiple points when dataset is large,
// so we need to find the first x to show tooltip.
const firstX = (_a = filteredSeriesData[0]) === null || _a === void 0 ? void 0 : _a[0].x;
const transformedX = firstX !== null && firstX !== void 0 ? firstX : abstractX(focus);
emitter.emit('tooltip:show', Object.assign(Object.assign({}, event), { nativeEvent: true, data: {
data: { x: (0, scale_2.invert)(scale.x, transformedX, true) },
} }));
}, wait, { leading, trailing });
const hide = (event) => {
hideTooltip({ root, single, emitter, event });
};
const destroy = () => {
destroyTooltip({ root, single });
};
const onTooltipShow = (_a) => {
var _b;
var { nativeEvent, data, offsetX, offsetY } = _a, rest = __rest(_a, ["nativeEvent", "data", "offsetX", "offsetY"]);
if (nativeEvent)
return;
const x = (_b = data === null || data === void 0 ? void 0 : data.data) === null || _b === void 0 ? void 0 : _b.x;
const scaleX = scale.x;
const x1 = scaleX.map(x);
const [x2, y2] = coordinate.map([x1, 0.5]);
const rootBounds = root.getRenderBounds();
const minX = rootBounds.min[0];
const minY = rootBounds.min[1];
update(Object.assign(Object.assign({}, rest), { offsetX: offsetX !== undefined ? offsetX : minX + x2, offsetY: offsetY !== undefined ? offsetY : minY + y2, _x: x }));
};
const onTooltipHide = () => {
hideTooltip({ root, single, emitter, nativeEvent: false });
};
const onTooltipDisable = () => {
removeEventListeners();
destroy();
};
const onTooltipEnable = () => {
addEventListeners();
};
const addEventListeners = () => {
if (!disableNative) {
root.addEventListener('pointerenter', update);
root.addEventListener('pointermove', update);
// Only emit pointerleave event when the pointer is not in the root area.
root.addEventListener('pointerleave', (e) => {
if ((0, utils_1.mousePosition)(root, e))
return;
hide(e);
});
}
};
const removeEventListeners = () => {
if (!disableNative) {
root.removeEventListener('pointerenter', update);
root.removeEventListener('pointermove', update);
root.removeEventListener('pointerleave', hide);
}
};
addEventListeners();
emitter.on('tooltip:show', onTooltipShow);
emitter.on('tooltip:hide', onTooltipHide);
emitter.on('tooltip:disable', onTooltipDisable);
emitter.on('tooltip:enable', onTooltipEnable);
return () => {
removeEventListeners();
emitter.off('tooltip:show', onTooltipShow);
emitter.off('tooltip:hide', onTooltipHide);
emitter.off('tooltip:disable', onTooltipDisable);
emitter.off('tooltip:enable', onTooltipEnable);
if (preserve) {
hideTooltip({ root, single, emitter, nativeEvent: false });
}
else {
destroy();
}
};
}
exports.seriesTooltip = seriesTooltip;
/**
* Show tooltip for non-series item.
*/
function tooltip(root, { elements: elementsof, coordinate, scale, render, groupName, sort: sortFunction, filter: filterFunction, emitter, wait = 50, leading = true, trailing = false, groupKey = (d) => d, // group elements by specified key
single = true, position, enterable, datum, view, mount, bounding, theme, offset, shared = false, body = true, disableNative = false, preserve = false, css = {}, }) {
var _a, _b;
const elements = elementsof(root);
const keyGroup = (0, d3_array_1.group)(elements, groupKey);
const inInterval = (d) => d.markType === 'interval';
const isBar = elements.every(inInterval) && !(0, coordinate_1.isPolar)(coordinate);
const scaleX = scale.x;
const scaleSeries = scale.series;
const bandWidth = (_b = (_a = scaleX === null || scaleX === void 0 ? void 0 : scaleX.getBandWidth) === null || _a === void 0 ? void 0 : _a.call(scaleX)) !== null && _b !== void 0 ? _b : 0;
const xof = scaleSeries
? (d) => d.__data__.x + d.__data__.series * bandWidth
: (d) => d.__data__.x + bandWidth / 2;
// Sort for bisector search.
if (isBar)
elements.sort((a, b) => xof(a) - xof(b));
const findElementByTarget = (event) => {
const { target } = event;
return (0, utils_1.maybeRoot)(target, (node) => {
if (!node.classList)
return false;
return node.classList.includes('element');
});
};
const findElement = isBar
? (event) => {
const mouse = (0, utils_1.mousePosition)(root, event);
if (!mouse)
return;
const [normalizedX] = coordinate.invert(mouse);
const abstractX = normalizedX;
const search = (0, d3_array_1.bisector)(xof).center;
const i = search(elements, abstractX);
const target = elements[i];
if (!shared) {
// For grouped bar chart without shared options.
const isGrouped = elements.find((d) => d !== target && xof(d) === xof(target));
if (isGrouped)
return findElementByTarget(event);
}
return target;
}
: findElementByTarget;
const pointermove = (0, util_1.throttle)((event) => {
const element = findElement(event);
if (!element) {
hideTooltip({ root, single, emitter, event });
return;
}
const k = groupKey(element);
const group = keyGroup.get(k);
if (!group) {
return;
}
const data = group.length === 1 && !shared
? singleItem(group[0])
: groupItems(group, scale, groupName, undefined, theme);
// Sort items and sort.
if (sortFunction) {
data.items.sort((a, b) => sortFunction(a) - sortFunction(b));
}
if (filterFunction) {
data.items = data.items.filter(filterFunction);
}
if (isEmptyTooltipData(data)) {
hideTooltip({ root, single, emitter, event });
return;
}
const { offsetX, offsetY } = event;
if (body) {
showTooltip({
root,
data,
x: offsetX,
y: offsetY,
render,
event,
single,
position,
enterable,
mount,
bounding,
css,
offset,
});
}
emitter.emit('tooltip:show', Object.assign(Object.assign({}, event), { nativeEvent: true, data: {
data: (0, event_1.dataOf)(element, view),
} }));
}, wait, { leading, trailing });
const pointerleave = (event) => {
hideTooltip({ root, single, emitter, event });
};
const addEventListeners = () => {
if (!disableNative) {
root.addEventListener('pointermove', pointermove);
// Only emit pointerleave event when the pointer is not in the root area.
// !!!DO NOT USE pointerout event, it will emit when the pointer is in the child area.
root.addEventListener('pointerleave', pointerleave);
}
};
const removeEventListeners = () => {
if (!disableNative) {
root.removeEventListener('pointermove', pointermove);
root.removeEventListener('pointerleave', pointerleave);
}
};
const onTooltipShow = ({ nativeEvent, offsetX, offsetY, data: raw }) => {
if (nativeEvent)
return;
const { data } = raw;
const element = (0, utils_1.selectElementByData)(elements, data, datum);
if (!element)
return;
const bbox = element.getBBox();
const { x, y, width, height } = bbox;
const rootBBox = root.getBBox();
pointermove({
target: element,
offsetX: offsetX !== undefined ? offsetX + rootBBox.x : x + width / 2,
offsetY: offsetY !== undefined ? offsetY + rootBBox.y : y + height / 2,
});
};
const onTooltipHide = ({ nativeEvent } = {}) => {
if (nativeEvent)
return;
hideTooltip({ root, single, emitter, nativeEvent: false });
};
const onTooltipDisable = () => {
removeEventListeners();
destroyTooltip({ root, single });
};
const onTooltipEnable = () => {
addEventListeners();
};
emitter.on('tooltip:show', onTooltipShow);
emitter.on('tooltip:hide', onTooltipHide);
emitter.on('tooltip:enable', onTooltipEnable);
emitter.on('tooltip:disable', onTooltipDisable);
addEventListeners();
return () => {
removeEventListeners();
emitter.off('tooltip:show', onTooltipShow);
emitter.off('tooltip:hide', onTooltipHide);
if (preserve) {
hideTooltip({ root, single, emitter, nativeEvent: false });
}
else {
destroyTooltip({ root, single });
}
};
}
exports.tooltip = tooltip;
function Tooltip(options) {
const { shared, crosshairs, crosshairsX, crosshairsY, series, name, item = () => ({}), facet = false } = options, rest = __rest(options, ["shared", "crosshairs", "crosshairsX", "crosshairsY", "series", "name", "item", "facet"]);
return (target, viewInstances, emitter) => {
const { container, view } = target;
const { scale, markState, coordinate, theme } = view;
// Get default value from mark states.
const defaultSeries = interactionKeyof(markState, 'seriesTooltip');
const defaultShowCrosshairs = interactionKeyof(markState, 'crosshairs');
const plotArea = (0, utils_1.selectPlotArea)(container);
const isSeries = maybeValue(series, defaultSeries);
const crosshairsSetting = maybeValue(crosshairs, defaultShowCrosshairs);
// For non-facet and series tooltip.
if (isSeries && hasSeries(markState) && !facet) {
return seriesTooltip(plotArea, Object.assign(Object.assign({}, rest), { theme, elements: utils_1.selectG2Elements, scale,
coordinate, crosshairs: crosshairsSetting,
// the crosshairsX settings level: crosshairsX > crosshairs > false
// it means crosshairsX default is false
crosshairsX: maybeValue(maybeValue(crosshairsX, crosshairs), false),
// crosshairsY default depend on the crossharisSettings
crosshairsY: maybeValue(crosshairsY, crosshairsSetting), item,
emitter }));
}
// For facet and series tooltip.
if (isSeries && facet) {
// Get sub view instances for this view.
const facetInstances = viewInstances.filter((d) => d !== target && d.options.parentKey === target.options.key);
const elements = (0, utils_1.selectFacetG2Elements)(target, viewInstances);
// Use the scale of the first view.
const scale = facetInstances[0].view.scale;
const bbox = plotArea.getBounds();
const startX = bbox.min[0];
const startY = bbox.min[1];
Object.assign(scale, { facet: true });
// @todo Nested structure rather than flat structure for facet?
// Add listener to the root area.
// @ts-ignore
return seriesTooltip(plotArea.parentNode.parentNode, Object.assign(Object.assign({}, rest), { theme, elements: () => elements, scale,
coordinate, crosshairs: maybeValue(crosshairs, defaultShowCrosshairs),
// the crosshairsX settings level: crosshairsX > crosshairs > false
// it means crosshairsX default is false
crosshairsX: maybeValue(maybeValue(crosshairsX, crosshairs), false), crosshairsY: maybeValue(crosshairsY, crosshairsSetting), item,
startX,
startY,
emitter }));
}
return tooltip(plotArea, Object.assign(Object.assign({}, rest), { datum: (0, utils_1.createDatumof)(view), elements: utils_1.selectG2Elements, scale,
coordinate, groupKey: shared ? (0, utils_1.createXKey)(view) : undefined, item,
emitter,
view,
theme,
shared }));
};
}
exports.Tooltip = Tooltip;
Tooltip.props = {
reapplyWhenUpdate: true,
};
//# sourceMappingURL=tooltip.js.map