tooltip.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. var __rest = (this && this.__rest) || function (s, e) {
  2. var t = {};
  3. for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
  4. t[p] = s[p];
  5. if (s != null && typeof Object.getOwnPropertySymbols === "function")
  6. for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
  7. if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
  8. t[p[i]] = s[p[i]];
  9. }
  10. return t;
  11. };
  12. import { Line } from '@antv/g';
  13. import { sort, group, mean, bisector, minIndex } from 'd3-array';
  14. import { deepMix, lowerFirst, throttle } from '@antv/util';
  15. import { Tooltip as TooltipComponent } from '@antv/gui';
  16. import { Constant, Identity } from '@antv/scale';
  17. import { defined, subObject } from '../utils/helper';
  18. import { isTranspose, isPolar } from '../utils/coordinate';
  19. import { angle, sub } from '../utils/vector';
  20. import { invert } from '../utils/scale';
  21. import { selectG2Elements, createXKey, selectPlotArea, mousePosition, selectFacetG2Elements, createDatumof, selectElementByData, } from './utils';
  22. import { dataOf } from './event';
  23. function getContainer(group, mount) {
  24. if (mount)
  25. return typeof mount === 'string' ? document.querySelector(mount) : mount;
  26. // @ts-ignore
  27. return group.getRootNode().defaultView.getConfig().container;
  28. }
  29. function getBounding(root) {
  30. const bbox = root.getBounds();
  31. const { min: [x1, y1], max: [x2, y2], } = bbox;
  32. return {
  33. x: x1,
  34. y: y1,
  35. width: x2 - x1,
  36. height: y2 - y1,
  37. };
  38. }
  39. function getContainerOffset(container1, container2) {
  40. const r1 = container1.getBoundingClientRect();
  41. const r2 = container2.getBoundingClientRect();
  42. return {
  43. x: r1.x - r2.x,
  44. y: r1.y - r2.y,
  45. };
  46. }
  47. function createTooltip(container, x0, y0, position, enterable, bounding, containerOffset) {
  48. const tooltipElement = new TooltipComponent({
  49. className: 'tooltip',
  50. style: {
  51. x: x0,
  52. y: y0,
  53. container: containerOffset,
  54. data: [],
  55. bounding,
  56. position,
  57. enterable,
  58. title: '',
  59. offset: [10, 10],
  60. template: {
  61. prefixCls: 'g2-',
  62. },
  63. style: {
  64. '.g2-tooltip': {},
  65. '.g2-tooltip-title': {
  66. overflow: 'hidden',
  67. 'white-space': 'nowrap',
  68. 'text-overflow': 'ellipsis',
  69. },
  70. },
  71. },
  72. });
  73. container.appendChild(tooltipElement.HTMLTooltipElement);
  74. return tooltipElement;
  75. }
  76. function showTooltip({ root, data, x, y, render, event, single, position = 'right-bottom', enterable = false, mount, bounding, }) {
  77. // All the views share the same tooltip.
  78. const canvasContainer = root.getRootNode().defaultView.getConfig().container;
  79. const container = single ? getContainer(root, mount) : root;
  80. const b = bounding || getBounding(root);
  81. const containerOffset = getContainerOffset(canvasContainer, container);
  82. const { tooltipElement = createTooltip(container, x, y, position, enterable, b, containerOffset), } = container;
  83. const { items, title = '' } = data;
  84. tooltipElement.update(Object.assign({ x,
  85. y, data: items, title,
  86. position,
  87. enterable }, (render !== undefined && {
  88. content: render(event, { items, title }),
  89. })));
  90. container.tooltipElement = tooltipElement;
  91. }
  92. function hideTooltip({ root, single, emitter, nativeEvent = true, mount }) {
  93. const container = single ? getContainer(root, mount) : root;
  94. const { tooltipElement } = container;
  95. if (tooltipElement) {
  96. tooltipElement.hide();
  97. if (nativeEvent) {
  98. emitter.emit('tooltip:hide', { nativeEvent });
  99. }
  100. }
  101. }
  102. function destroyTooltip(root) {
  103. const { tooltipElement } = root;
  104. if (tooltipElement) {
  105. tooltipElement.destroy();
  106. root.tooltipElement = undefined;
  107. }
  108. }
  109. function showUndefined(item) {
  110. const { value } = item;
  111. return Object.assign(Object.assign({}, item), { value: value === undefined ? 'undefined' : value });
  112. }
  113. function singleItem(element) {
  114. const { __data__: datum } = element;
  115. const { title, items = [] } = datum;
  116. const newItems = items
  117. .filter(defined)
  118. .map((_a) => {
  119. var { color = itemColorOf(element) } = _a, item = __rest(_a, ["color"]);
  120. return (Object.assign(Object.assign({}, item), { color }));
  121. })
  122. .map(showUndefined);
  123. return Object.assign(Object.assign({}, (title && { title })), { items: newItems });
  124. }
  125. function groupNameOf(scale, datum) {
  126. const { color: scaleColor, series: scaleSeries } = scale;
  127. const { color, series } = datum;
  128. const invertAble = (scale) => {
  129. return (scale &&
  130. scale.invert &&
  131. !(scale instanceof Identity) &&
  132. !(scale instanceof Constant));
  133. };
  134. // For non constant color channel.
  135. if (invertAble(scaleSeries))
  136. return scaleSeries.invert(series);
  137. if (series && series !== color)
  138. return series;
  139. if (invertAble(scaleColor)) {
  140. const name = scaleColor.invert(color);
  141. // For threshold scale.
  142. if (Array.isArray(name))
  143. return null;
  144. return name;
  145. }
  146. return null;
  147. }
  148. function itemColorOf(element) {
  149. const fill = element.getAttribute('fill');
  150. const stroke = element.getAttribute('stroke');
  151. const { __data__: datum } = element;
  152. const { color = fill && fill !== 'transparent' ? fill : stroke } = datum;
  153. return color;
  154. }
  155. function unique(items, key = (d) => d) {
  156. const valueName = new Map(items.map((d) => [key(d), d]));
  157. return Array.from(valueName.values());
  158. }
  159. function groupItems(elements, scale, groupName, data = elements.map((d) => d['__data__'])) {
  160. const key = (d) => (d instanceof Date ? +d : d);
  161. const T = unique(data.map((d) => d.title), key).filter(defined);
  162. const newItems = data
  163. .flatMap((datum, i) => {
  164. const element = elements[i];
  165. const { items = [], title } = datum;
  166. const definedItems = items.filter(defined);
  167. // If there is only one item, use groupName as title by default.
  168. const useGroupName = groupName !== undefined ? groupName : items.length <= 1 ? true : false;
  169. return definedItems.map((_a) => {
  170. var { color = itemColorOf(element), name } = _a, item = __rest(_a, ["color", "name"]);
  171. const name1 = useGroupName
  172. ? groupNameOf(scale, datum) || name
  173. : name || groupNameOf(scale, datum);
  174. return Object.assign(Object.assign({}, item), { color, name: name1 || title });
  175. });
  176. })
  177. .map(showUndefined);
  178. return Object.assign(Object.assign({}, (T.length > 0 && { title: T.join(',') })), { items: unique(newItems, (d) => `(${key(d.name)}, ${key(d.value)}, ${key(d.color)})`) });
  179. }
  180. function updateRuleY(root, points, _a) {
  181. var { height, width, startX, startY, transposed, polar } = _a, rest = __rest(_a, ["height", "width", "startX", "startY", "transposed", "polar"]);
  182. const defaults = Object.assign({ lineWidth: 1, stroke: '#1b1e23', strokeOpacity: 0.5 }, rest);
  183. const Y = points.map((p) => p[1]);
  184. const X = points.map((p) => p[0]);
  185. const y = mean(Y);
  186. const x = mean(X);
  187. const pointsOf = () => {
  188. if (polar) {
  189. const cx = startX + width / 2;
  190. const cy = startY + height / 2;
  191. const r = Math.min(width, height) / 2;
  192. const a = angle(sub([x, y], [cx, cy]));
  193. const x0 = cx + r * Math.cos(a);
  194. const y0 = cy + r * Math.sin(a);
  195. return [cx, x0, cy, y0];
  196. }
  197. if (transposed) {
  198. return [startX, startX + width, y + startY, y + startY];
  199. }
  200. return [x + startX, x + startX, startY, startY + height];
  201. };
  202. const [x1, x2, y1, y2] = pointsOf();
  203. const createLine = () => {
  204. const line = new Line({
  205. style: Object.assign({ x1,
  206. x2,
  207. y1,
  208. y2 }, defaults),
  209. });
  210. root.appendChild(line);
  211. return line;
  212. };
  213. const ruleY = root.ruleY || createLine();
  214. ruleY.style.x1 = x1;
  215. ruleY.style.x2 = x2;
  216. ruleY.style.y1 = y1;
  217. ruleY.style.y2 = y2;
  218. root.ruleY = ruleY;
  219. }
  220. function hideRuleY(root) {
  221. if (root.ruleY) {
  222. root.ruleY.remove();
  223. root.ruleY = undefined;
  224. }
  225. }
  226. function interactionKeyof(markState, key) {
  227. return Array.from(markState.values()).some(
  228. // @ts-ignore
  229. (d) => { var _a; return (_a = d.interaction) === null || _a === void 0 ? void 0 : _a[key]; });
  230. }
  231. function maybeValue(specified, defaults) {
  232. return specified === undefined ? defaults : specified;
  233. }
  234. function isEmptyTooltipData(data) {
  235. const { title, items } = data;
  236. if (items.length === 0 && title === undefined)
  237. return true;
  238. return false;
  239. }
  240. function hasSeries(markState) {
  241. return Array.from(markState.values()).some(
  242. // @ts-ignore
  243. (d) => { var _a; return ((_a = d.interaction) === null || _a === void 0 ? void 0 : _a.seriesTooltip) && d.tooltip; });
  244. }
  245. /**
  246. * Show tooltip for series item.
  247. */
  248. export function seriesTooltip(root, _a) {
  249. 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, 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", "style"]);
  250. const elements = elementsof(root);
  251. const transposed = isTranspose(coordinate);
  252. const polar = isPolar(coordinate);
  253. const style = deepMix(_style, rest);
  254. const { innerWidth: width, innerHeight: height } = coordinate.getOptions();
  255. // Split elements into series elements and item elements.
  256. const seriesElements = [];
  257. const itemElements = [];
  258. for (const element of elements) {
  259. const { __data__: data } = element;
  260. const { seriesX } = data;
  261. if (seriesX)
  262. seriesElements.push(element);
  263. else
  264. itemElements.push(element);
  265. }
  266. // Sorted elements from top to bottom visually,
  267. // or from right to left in transpose coordinate.
  268. seriesElements.sort((a, b) => {
  269. const index = transposed ? 0 : 1;
  270. const minY = (d) => d.getBounds().min[index];
  271. return transposed ? minY(b) - minY(a) : minY(a) - minY(b);
  272. });
  273. // Get sortedIndex and X for each series elements
  274. const elementSortedX = new Map(seriesElements.map((element) => {
  275. const { __data__: data } = element;
  276. const { seriesX } = data;
  277. const seriesIndex = seriesX.map((_, i) => i);
  278. const sortedIndex = sort(seriesIndex, (i) => seriesX[+i]);
  279. return [element, [sortedIndex, seriesX]];
  280. }));
  281. const ruleStyle = subObject(style, 'crosshairs');
  282. const { x: scaleX } = scale;
  283. // Apply offset for band scale x.
  284. const offsetX = (scaleX === null || scaleX === void 0 ? void 0 : scaleX.getBandWidth) ? scaleX.getBandWidth() / 2 : 0;
  285. const abstractX = (focus) => {
  286. const [normalizedX] = coordinate.invert(focus);
  287. return normalizedX - offsetX;
  288. };
  289. const indexByFocus = (focus, I, X) => {
  290. const finalX = abstractX(focus);
  291. const [minX, maxX] = sort([X[0], X[X.length - 1]]);
  292. // Skip x out of range.
  293. if (finalX < minX || finalX > maxX)
  294. return null;
  295. const search = bisector((i) => X[+i]).center;
  296. const i = search(I, finalX);
  297. return I[i];
  298. };
  299. const elementsByFocus = (focus, elements) => {
  300. const index = transposed ? 1 : 0;
  301. const x = focus[index];
  302. const extent = (d) => {
  303. const { min, max } = d.getLocalBounds();
  304. return sort([min[index], max[index]]);
  305. };
  306. return elements.filter((element) => {
  307. const [min, max] = extent(element);
  308. return x >= min && x <= max;
  309. });
  310. };
  311. const seriesData = (element, index) => {
  312. const { __data__: data } = element;
  313. return Object.fromEntries(Object.entries(data)
  314. .filter(([key]) => key.startsWith('series') && key !== 'series')
  315. .map(([key, V]) => {
  316. const d = V[index];
  317. return [lowerFirst(key.replace('series', '')), d];
  318. }));
  319. };
  320. const update = throttle((event) => {
  321. const mouse = mousePosition(root, event);
  322. if (!mouse)
  323. return;
  324. const bbox = root.getRenderBounds();
  325. const x = bbox.min[0];
  326. const y = bbox.min[1];
  327. const focus = [mouse[0] - startX, mouse[1] - startY];
  328. if (!focus)
  329. return;
  330. // Get selected item element.
  331. const selectedItems = elementsByFocus(focus, itemElements);
  332. // Get selected data item from both series element and item element.
  333. const selectedSeriesElements = [];
  334. const selectedSeriesData = [];
  335. for (const element of seriesElements) {
  336. const [sortedIndex, X] = elementSortedX.get(element);
  337. const index = indexByFocus(focus, sortedIndex, X);
  338. if (index !== null) {
  339. selectedSeriesElements.push(element);
  340. const d = seriesData(element, index);
  341. const { x, y } = d;
  342. const p = coordinate.map([(x || 0) + offsetX, y || 0]);
  343. selectedSeriesData.push([d, p]);
  344. }
  345. }
  346. // Filter selectedSeriesData with different x,
  347. // make sure there is only one x closest to focusX.
  348. const SX = Array.from(new Set(selectedSeriesData.map((d) => d[0].x)));
  349. const closestX = SX[minIndex(SX, (x) => Math.abs(x - abstractX(focus)))];
  350. const filteredSeriesData = selectedSeriesData.filter((d) => d[0].x === closestX);
  351. const selectedData = [
  352. ...filteredSeriesData.map((d) => d[0]),
  353. ...selectedItems.map((d) => d.__data__),
  354. ];
  355. // Get the displayed tooltip data.
  356. const selectedElements = [...selectedSeriesElements, ...selectedItems];
  357. const tooltipData = groupItems(selectedElements, scale, groupName, selectedData);
  358. // Sort items and filter items.
  359. if (sortFunction) {
  360. tooltipData.items.sort((a, b) => sortFunction(a) - sortFunction(b));
  361. }
  362. if (filterFunction) {
  363. tooltipData.items = tooltipData.items.filter(filterFunction);
  364. }
  365. // Hide tooltip with no selected tooltip.
  366. if (selectedElements.length === 0 || isEmptyTooltipData(tooltipData)) {
  367. hide();
  368. return;
  369. }
  370. if (body) {
  371. showTooltip({
  372. root,
  373. data: tooltipData,
  374. x: mouse[0] + x,
  375. y: mouse[1] + y,
  376. render,
  377. event,
  378. single,
  379. position,
  380. enterable,
  381. mount,
  382. bounding,
  383. });
  384. }
  385. if (crosshairs) {
  386. const points = filteredSeriesData.map((d) => d[1]);
  387. updateRuleY(root, points, Object.assign(Object.assign({}, ruleStyle), { width,
  388. height,
  389. startX,
  390. startY,
  391. transposed,
  392. polar }));
  393. }
  394. emitter.emit('tooltip:show', Object.assign(Object.assign({}, event), { nativeEvent: true, data: { data: { x: invert(scale.x, abstractX(focus), true) } } }));
  395. }, wait, { leading, trailing });
  396. const hide = () => {
  397. hideTooltip({ root, single, emitter, mount });
  398. if (crosshairs)
  399. hideRuleY(root);
  400. };
  401. const onTooltipShow = ({ nativeEvent, data }) => {
  402. if (nativeEvent)
  403. return;
  404. const { x } = data.data;
  405. const { x: scaleX } = scale;
  406. const x1 = scaleX.map(x);
  407. const [x2, y2] = coordinate.map([x1, 0.5]);
  408. const { min: [minX, minY], } = root.getRenderBounds();
  409. update({ offsetX: x2 + minX, offsetY: y2 + minY });
  410. };
  411. const onTooltipHide = () => {
  412. hideTooltip({ root, single, emitter, nativeEvent: false, mount });
  413. };
  414. emitter.on('tooltip:show', onTooltipShow);
  415. emitter.on('tooltip:hide', onTooltipHide);
  416. root.addEventListener('pointerenter', update);
  417. root.addEventListener('pointermove', update);
  418. root.addEventListener('pointerleave', hide);
  419. return () => {
  420. root.removeEventListener('pointerenter', update);
  421. root.removeEventListener('pointermove', update);
  422. root.removeEventListener('pointerleave', hide);
  423. emitter.off('tooltip:show', onTooltipShow);
  424. emitter.off('tooltip:hide', onTooltipHide);
  425. destroyTooltip(root);
  426. if (crosshairs)
  427. hideRuleY(root);
  428. };
  429. }
  430. /**
  431. * Show tooltip for non-series item.
  432. */
  433. export 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
  434. single = true, position, enterable, datum, view, mount, bounding, }) {
  435. const elements = elementsof(root);
  436. const elementSet = new Set(elements);
  437. const keyGroup = group(elements, groupKey);
  438. const pointerover = throttle((event) => {
  439. const { target: element } = event;
  440. if (!elementSet.has(element)) {
  441. hideTooltip({ root, single, emitter, mount });
  442. return;
  443. }
  444. const k = groupKey(element);
  445. const group = keyGroup.get(k);
  446. const data = group.length === 1
  447. ? singleItem(group[0])
  448. : groupItems(group, scale, groupName);
  449. // Sort items and sort.
  450. if (sortFunction) {
  451. data.items.sort((a, b) => sortFunction(a) - sortFunction(b));
  452. }
  453. if (filterFunction) {
  454. data.items = data.items.filter(filterFunction);
  455. }
  456. if (isEmptyTooltipData(data)) {
  457. hideTooltip({ root, single, emitter, mount });
  458. return;
  459. }
  460. const { offsetX, offsetY } = event;
  461. showTooltip({
  462. root,
  463. data,
  464. x: offsetX,
  465. y: offsetY,
  466. render,
  467. event,
  468. single,
  469. position,
  470. enterable,
  471. mount,
  472. bounding,
  473. });
  474. emitter.emit('tooltip:show', Object.assign(Object.assign({}, event), { nativeEvent: true, data: {
  475. data: dataOf(element, view),
  476. } }));
  477. }, wait, { leading, trailing });
  478. const pointerout = (event) => {
  479. const { target: element } = event;
  480. if (!elementSet.has(element))
  481. return;
  482. hideTooltip({ root, single, emitter, mount });
  483. };
  484. const onTooltipShow = ({ nativeEvent, data }) => {
  485. if (nativeEvent)
  486. return;
  487. const element = selectElementByData(elements, data.data, datum);
  488. if (!element)
  489. return;
  490. const bbox = element.getBBox();
  491. const { x, y, width, height } = bbox;
  492. pointerover({
  493. target: element,
  494. offsetX: x + width / 2,
  495. offsetY: y + height / 2,
  496. });
  497. };
  498. const onTooltipHide = ({ nativeEvent } = {}) => {
  499. if (nativeEvent)
  500. return;
  501. hideTooltip({ root, single, emitter, nativeEvent: false, mount });
  502. };
  503. emitter.on('tooltip:show', onTooltipShow);
  504. emitter.on('tooltip:hide', onTooltipHide);
  505. root.addEventListener('pointerover', pointerover);
  506. root.addEventListener('pointermove', pointerover);
  507. root.addEventListener('pointerout', pointerout);
  508. return () => {
  509. root.removeEventListener('pointerover', pointerover);
  510. root.removeEventListener('pointermove', pointerover);
  511. root.removeEventListener('pointerout', pointerout);
  512. emitter.off('tooltip:show', onTooltipShow);
  513. emitter.off('tooltip:hide', onTooltipHide);
  514. destroyTooltip(root);
  515. };
  516. }
  517. export function Tooltip(options) {
  518. const { shared, crosshairs, series, name, item = () => ({}), facet = false } = options, rest = __rest(options, ["shared", "crosshairs", "series", "name", "item", "facet"]);
  519. return (target, viewInstances, emitter) => {
  520. const { container, view } = target;
  521. const { scale, markState, coordinate } = view;
  522. // Get default value from mark states.
  523. const defaultSeries = interactionKeyof(markState, 'seriesTooltip');
  524. const defaultShowCrosshairs = interactionKeyof(markState, 'crosshairs');
  525. const plotArea = selectPlotArea(container);
  526. const isSeries = maybeValue(series, defaultSeries);
  527. // For non-facet and series tooltip.
  528. if (isSeries && hasSeries(markState) && !facet) {
  529. return seriesTooltip(plotArea, Object.assign(Object.assign({}, rest), { elements: selectG2Elements, scale,
  530. coordinate, crosshairs: maybeValue(crosshairs, defaultShowCrosshairs), item,
  531. emitter }));
  532. }
  533. // For facet and series tooltip.
  534. if (isSeries && facet) {
  535. // Get sub view instances for this view.
  536. const facetInstances = viewInstances.filter((d) => d !== target && d.options.parentKey === target.options.key);
  537. const elements = selectFacetG2Elements(target, viewInstances);
  538. // Use the scale of the first view.
  539. const scale = facetInstances[0].view.scale;
  540. const bbox = plotArea.getBounds();
  541. const startX = bbox.min[0];
  542. const startY = bbox.min[1];
  543. // @todo Nested structure rather than flat structure for facet?
  544. // Add listener to the root area.
  545. // @ts-ignore
  546. return seriesTooltip(plotArea.parentNode.parentNode, Object.assign(Object.assign({}, rest), { elements: () => elements, scale,
  547. coordinate, crosshairs: maybeValue(crosshairs, defaultShowCrosshairs), item,
  548. startX,
  549. startY,
  550. emitter }));
  551. }
  552. return tooltip(plotArea, Object.assign(Object.assign({}, rest), { datum: createDatumof(view), elements: selectG2Elements, scale,
  553. coordinate, groupKey: shared ? createXKey(view) : undefined, item,
  554. emitter,
  555. view }));
  556. };
  557. }
  558. //# sourceMappingURL=tooltip.js.map