UNPKG

@antv/g2

Version:

the Grammar of Graphics in Javascript

664 lines 26.7 kB
"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 gui_1 = require("@antv/gui"); 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; // @ts-ignore return group.getRootNode().defaultView.getConfig().container; } function getBounding(root) { const bbox = root.getBounds(); 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) { const tooltipElement = new gui_1.Tooltip({ className: 'tooltip', style: { x: x0, y: y0, container: containerOffset, data: [], bounding, position, enterable, title: '', offset: [10, 10], template: { prefixCls: 'g2-', }, style: { '.g2-tooltip': {}, '.g2-tooltip-title': { overflow: 'hidden', 'white-space': 'nowrap', 'text-overflow': 'ellipsis', }, }, }, }); container.appendChild(tooltipElement.HTMLTooltipElement); return tooltipElement; } function showTooltip({ root, data, x, y, render, event, single, position = 'right-bottom', enterable = false, mount, bounding, }) { const canvasContainer = root.getRootNode().defaultView.getConfig().container; const container = getContainer(root, mount); // 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), } = 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 }) { if (nativeEvent) { emitter.emit('tooltip:hide', { nativeEvent }); } const canvasContainer = root.getRootNode().defaultView.getConfig().container; const parent = single ? canvasContainer : root; const { tooltipElement } = parent; if (tooltipElement) { tooltipElement.hide(); } } function destroyTooltip({ root, single }) { const canvasContainer = root.getRootNode().defaultView.getConfig().container; const parent = single ? canvasContainer : root; if (!parent) return; const { tooltipElement } = parent; if (tooltipElement) { tooltipElement.destroy(); parent.tooltipElement = undefined; } } 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)) return scaleSeries.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__'])) { 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), name } = _a, item = __rest(_a, ["color", "name"]); const name1 = useGroupName ? groupNameOf(scale, datum) || name : name || groupNameOf(scale, datum); 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 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; } } // @todo Fill for composite g component. function updateMarker(root, { data, style }) { if (root.markers) root.markers.forEach((d) => d.remove()); const markers = data.map((d) => { const [{ color, element }, point] = d; const fill = color || // encode value element.style.fill || element.style.stroke || '#000'; const shape = new g_1.Circle({ style: Object.assign({ cx: point[0], cy: point[1], fill, r: 4, stroke: '#fff', strokeWidth: 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, render, groupName, emitter, wait = 50, leading = true, trailing = false, startX = 0, startY = 0, body = true, single = true, position, enterable, mount, bounding, disableNative = false, marker = true, style: _style = {} } = _a, rest = __rest(_a, ["elements", "sort", "filter", "scale", "coordinate", "crosshairs", "render", "groupName", "emitter", "wait", "leading", "trailing", "startX", "startY", "body", "single", "position", "enterable", "mount", "bounding", "disableNative", "marker", "style"]); 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 } = data; if (seriesX) seriesElements.push(element); else itemElements.push(element); } // 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); }); // 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 = (focus, I, X) => { const finalX = abstractX(focus); const [minX, maxX] = (0, d3_array_1.sort)([X[0], X[X.length - 1]]); // Skip x out of range. if (finalX < minX || finalX > maxX) return null; const search = (0, d3_array_1.bisector)((i) => X[+i]).center; const i = search(I, finalX); return I[i]; }; const elementsByFocus = (focus, elements) => { const index = transposed ? 1 : 0; const x = focus[index]; const extent = (d) => { const { min, max } = d.getLocalBounds(); return (0, d3_array_1.sort)([min[index], max[index]]); }; return elements.filter((element) => { const [min, max] = extent(element); return x >= min && x <= max; }); }; 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) => { const mouse = (0, utils_1.mousePosition)(root, event); if (!mouse) return; const bbox = root.getRenderBounds(); 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(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); // 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(); return; } if (body) { showTooltip({ root, data: tooltipData, x: mouse[0] + x, y: mouse[1] + y, render, event, single, position, enterable, mount, bounding, }); } if (crosshairs) { const points = filteredSeriesData.map((d) => d[1]); const ruleStyle = (0, helper_1.subObject)(style, 'crosshairs'); updateRuleY(root, points, Object.assign(Object.assign({}, ruleStyle), { 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, }); } emitter.emit('tooltip:show', Object.assign(Object.assign({}, event), { nativeEvent: true, data: { data: { x: (0, scale_2.invert)(scale.x, abstractX(focus), true) } } })); }, wait, { leading, trailing }); const hide = () => { hideTooltip({ root, single, emitter }); if (crosshairs) hideRuleY(root); if (marker) hideMarker(root); }; const destroy = () => { destroyTooltip({ root, single }); if (crosshairs) hideRuleY(root); if (marker) hideMarker(root); }; const onTooltipShow = ({ nativeEvent, data }) => { if (nativeEvent) return; const { x } = data.data; const { x: scaleX } = scale; const x1 = scaleX.map(x); const [x2, y2] = coordinate.map([x1, 0.5]); const { min: [minX, minY], } = root.getRenderBounds(); update({ offsetX: x2 + minX, offsetY: y2 + minY }); }; 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); root.addEventListener('pointerleave', hide); } }; 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); destroy(); }; } exports.seriesTooltip = seriesTooltip; /** * Show tooltip for non-series item. */ function tooltip(root, { elements: elementsof, 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, shared = false, body = true, disableNative = false, }) { const elements = elementsof(root); const elementSet = new Set(elements); const keyGroup = (0, d3_array_1.group)(elements, groupKey); const pointerover = (0, util_1.throttle)((event) => { const { target: element } = event; if (!elementSet.has(element)) { hideTooltip({ root, single, emitter }); return; } const k = groupKey(element); const group = keyGroup.get(k); const data = group.length === 1 && !shared ? singleItem(group[0]) : groupItems(group, scale, groupName); // 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 }); return; } const { offsetX, offsetY } = event; if (body) { showTooltip({ root, data, x: offsetX, y: offsetY, render, event, single, position, enterable, mount, bounding, }); } emitter.emit('tooltip:show', Object.assign(Object.assign({}, event), { nativeEvent: true, data: { data: (0, event_1.dataOf)(element, view), } })); }, wait, { leading, trailing }); const pointerout = (event) => { const { target: element } = event; if (!elementSet.has(element)) return; hideTooltip({ root, single, emitter }); }; const addEventListeners = () => { if (!disableNative) { root.addEventListener('pointerover', pointerover); root.addEventListener('pointermove', pointerover); root.addEventListener('pointerout', pointerout); } }; const removeEventListeners = () => { if (!disableNative) { root.removeEventListener('pointerover', pointerover); root.removeEventListener('pointermove', pointerover); root.removeEventListener('pointerout', pointerout); } destroyTooltip({ root, single }); }; const onTooltipShow = ({ nativeEvent, data }) => { if (nativeEvent) return; const element = (0, utils_1.selectElementByData)(elements, data.data, datum); if (!element) return; const bbox = element.getBBox(); const { x, y, width, height } = bbox; pointerover({ target: element, offsetX: x + width / 2, offsetY: 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); destroyTooltip({ root, single }); }; } exports.tooltip = tooltip; function Tooltip(options) { const { shared, crosshairs, series, name, item = () => ({}), facet = false } = options, rest = __rest(options, ["shared", "crosshairs", "series", "name", "item", "facet"]); return (target, viewInstances, emitter) => { const { container, view } = target; const { scale, markState, coordinate } = 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); // For non-facet and series tooltip. if (isSeries && hasSeries(markState) && !facet) { return seriesTooltip(plotArea, Object.assign(Object.assign({}, rest), { elements: utils_1.selectG2Elements, scale, coordinate, crosshairs: maybeValue(crosshairs, defaultShowCrosshairs), 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), { elements: () => elements, scale, coordinate, crosshairs: maybeValue(crosshairs, defaultShowCrosshairs), 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, shared })); }; } exports.Tooltip = Tooltip; //# sourceMappingURL=tooltip.js.map