diff --git a/__tests__/integration/api-chart-emit-item-tooltip.spec.ts b/__tests__/integration/api-chart-emit-item-tooltip.spec.ts
new file mode 100644
index 0000000000..7a26044213
--- /dev/null
+++ b/__tests__/integration/api-chart-emit-item-tooltip.spec.ts
@@ -0,0 +1,51 @@
+import { chartEmitItemTooltip as render } from '../plots/api/chart-emit-item-tooltip';
+import { kebabCase } from './utils/kebabCase';
+import {
+ dispatchFirstElementEvent,
+ createPromise,
+ receiveExpectData,
+} from './utils/event';
+import './utils/useSnapshotMatchers';
+import { createDOMGCanvas } from './utils/createDOMGCanvas';
+
+describe('chart.emit', () => {
+ const dir = `${__dirname}/snapshots/api/${kebabCase(render.name)}`;
+ const canvas = createDOMGCanvas(800, 500);
+
+ it('chart.emit and chart.on should control item tooltip display.', async () => {
+ const { finished, chart, clear } = render({
+ canvas,
+ container: document.createElement('div'),
+ });
+ await finished;
+ clear();
+
+ // chart.emit("tooltip:show", options) should show tooltip.
+ await expect(canvas).toMatchDOMSnapshot(dir, 'step0', {
+ selector: '.tooltip',
+ });
+
+ // chart.emit("tooltip:hide") should hide tooltip.
+ chart.emit('tooltip:hide');
+ await expect(canvas).toMatchDOMSnapshot(dir, 'step1', {
+ selector: '.tooltip',
+ });
+
+ chart.off();
+ // chart.on("tooltip:show", callback) should revive selected data.
+ const [tooltipShowed, resolveShow] = createPromise();
+ chart.on('tooltip:show', receiveExpectData(resolveShow));
+ dispatchFirstElementEvent(canvas, 'pointerover');
+ await tooltipShowed;
+
+ // chart.on("tooltip:hide") should be called when hiding tooltip.
+ const [tooltipHided, resolveHide] = createPromise();
+ chart.on('tooltip:hide', receiveExpectData(resolveHide, null));
+ dispatchFirstElementEvent(canvas, 'pointerout');
+ await tooltipHided;
+ });
+
+ afterAll(() => {
+ canvas?.destroy();
+ });
+});
diff --git a/__tests__/integration/api-chart-emit-pie-tooltip.spec.ts b/__tests__/integration/api-chart-emit-pie-tooltip.spec.ts
new file mode 100644
index 0000000000..291d07c5f1
--- /dev/null
+++ b/__tests__/integration/api-chart-emit-pie-tooltip.spec.ts
@@ -0,0 +1,27 @@
+import { chartEmitPieTooltip as render } from '../plots/api/chart-emit-pie-tooltip';
+import { kebabCase } from './utils/kebabCase';
+import './utils/useSnapshotMatchers';
+import { createDOMGCanvas } from './utils/createDOMGCanvas';
+
+describe('chart.emit', () => {
+ const dir = `${__dirname}/snapshots/api/${kebabCase(render.name)}`;
+ const canvas = createDOMGCanvas(800, 500);
+
+ it('chart.emit and chart.on should control item tooltip display.', async () => {
+ const { finished, chart, clear } = render({
+ canvas,
+ container: document.createElement('div'),
+ });
+ await finished;
+ clear();
+
+ // chart.emit("tooltip:show", options) should show tooltip.
+ await expect(canvas).toMatchDOMSnapshot(dir, 'step0', {
+ selector: '.tooltip',
+ });
+ });
+
+ afterAll(() => {
+ canvas?.destroy();
+ });
+});
diff --git a/__tests__/integration/api-chart-emit-series-tooltip.spec.ts b/__tests__/integration/api-chart-emit-series-tooltip.spec.ts
new file mode 100644
index 0000000000..7833deb7bc
--- /dev/null
+++ b/__tests__/integration/api-chart-emit-series-tooltip.spec.ts
@@ -0,0 +1,59 @@
+import { chartEmitSeriesTooltip as render } from '../plots/api/chart-emit-series-tooltip';
+import { kebabCase } from './utils/kebabCase';
+import {
+ dispatchPlotEvent,
+ createPromise,
+ receiveExpectData,
+} from './utils/event';
+import { createDOMGCanvas } from './utils/createDOMGCanvas';
+import './utils/useCustomFetch';
+import './utils/useSnapshotMatchers';
+
+describe('chart.emit', () => {
+ const dir = `${__dirname}/snapshots/api/${kebabCase(render.name)}`;
+ const canvas = createDOMGCanvas(800, 500);
+
+ it('chart.emit and chart.on should control item tooltip display.', async () => {
+ const { finished, chart, clear } = render({
+ canvas,
+ container: document.createElement('div'),
+ });
+ await finished;
+ clear();
+
+ // chart.emit("tooltip:show", options) should show tooltip.
+ await expect(canvas).toMatchDOMSnapshot(dir, 'step0', {
+ selector: '.tooltip',
+ });
+
+ // chart.emit("tooltip:hide") should hide tooltip.
+ chart.emit('tooltip:hide');
+ await expect(canvas).toMatchDOMSnapshot(dir, 'step1', {
+ selector: '.tooltip',
+ });
+
+ chart.off();
+ // chart.on("tooltip:show", callback) should revive selected data.
+ const [tooltipShowed, resolveShow] = createPromise();
+ chart.on('tooltip:show', (event) => {
+ const { x } = event.data.data;
+ expect(x.toUTCString()).toBe('Tue, 23 Oct 2007 05:18:47 GMT');
+ resolveShow();
+ });
+ dispatchPlotEvent(canvas, 'pointermove', {
+ offsetX: 100,
+ offsetY: 100,
+ });
+ await tooltipShowed;
+
+ // chart.on("tooltip:hide") should be called when hiding tooltip.
+ const [tooltipHided, resolveHide] = createPromise();
+ chart.on('tooltip:hide', receiveExpectData(resolveHide, null));
+ dispatchPlotEvent(canvas, 'pointerleave');
+ await tooltipHided;
+ });
+
+ afterAll(() => {
+ canvas?.destroy();
+ });
+});
diff --git a/__tests__/integration/api-chart-on-item-element.spec.ts b/__tests__/integration/api-chart-on-item-element.spec.ts
index 8ab0332b2d..95eccf7d23 100644
--- a/__tests__/integration/api-chart-on-item-element.spec.ts
+++ b/__tests__/integration/api-chart-on-item-element.spec.ts
@@ -1,6 +1,10 @@
import { chartOnItemElement as render } from '../plots/api/chart-on-item-element';
import { createDOMGCanvas } from './utils/createDOMGCanvas';
-import { dispatchEvent, createPromise, receiveExpectData } from './utils/event';
+import {
+ dispatchFirstElementEvent,
+ createPromise,
+ receiveExpectData,
+} from './utils/event';
import './utils/useSnapshotMatchers';
import { ChartEvent } from '../../src';
@@ -14,7 +18,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`element:${ChartEvent.CLICK}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'click', { detail: 1 });
+ dispatchFirstElementEvent(canvas, 'click', { detail: 1 });
await fired;
});
@@ -22,7 +26,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`interval:${ChartEvent.CLICK}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'click', { detail: 1 });
+ dispatchFirstElementEvent(canvas, 'click', { detail: 1 });
await fired;
});
@@ -30,7 +34,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`interval:${ChartEvent.DBLCLICK}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'click', { detail: 2 });
+ dispatchFirstElementEvent(canvas, 'click', { detail: 2 });
await fired;
});
@@ -38,7 +42,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`interval:${ChartEvent.POINTER_TAP}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'pointertap');
+ dispatchFirstElementEvent(canvas, 'pointertap');
await fired;
});
@@ -46,7 +50,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`interval:${ChartEvent.POINTER_DOWN}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'pointerdown');
+ dispatchFirstElementEvent(canvas, 'pointerdown');
await fired;
});
@@ -54,7 +58,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`interval:${ChartEvent.POINTER_UP}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'pointerup');
+ dispatchFirstElementEvent(canvas, 'pointerup');
await fired;
});
@@ -62,7 +66,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`interval:${ChartEvent.POINTER_OVER}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'pointerover');
+ dispatchFirstElementEvent(canvas, 'pointerover');
await fired;
});
@@ -70,7 +74,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`interval:${ChartEvent.POINTER_OUT}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'pointerout');
+ dispatchFirstElementEvent(canvas, 'pointerout');
await fired;
});
@@ -78,7 +82,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`interval:${ChartEvent.POINTER_MOVE}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'pointermove');
+ dispatchFirstElementEvent(canvas, 'pointermove');
await fired;
});
@@ -89,7 +93,7 @@ describe('chart.on', () => {
`interval:${ChartEvent.POINTER_ENTER}`,
receiveExpectData(resolve),
);
- dispatchEvent(canvas, 'pointerenter');
+ dispatchFirstElementEvent(canvas, 'pointerenter');
await fired;
});
@@ -100,7 +104,7 @@ describe('chart.on', () => {
`interval:${ChartEvent.POINTER_LEAVE}`,
receiveExpectData(resolve),
);
- dispatchEvent(canvas, 'pointerleave');
+ dispatchFirstElementEvent(canvas, 'pointerleave');
await fired;
});
@@ -111,7 +115,7 @@ describe('chart.on', () => {
`interval:${ChartEvent.POINTER_UPOUTSIDE}`,
receiveExpectData(resolve),
);
- dispatchEvent(canvas, 'pointerupoutside');
+ dispatchFirstElementEvent(canvas, 'pointerupoutside');
await fired;
});
@@ -119,7 +123,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`interval:${ChartEvent.DRAG_START}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'dragstart');
+ dispatchFirstElementEvent(canvas, 'dragstart');
await fired;
});
@@ -127,7 +131,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`interval:${ChartEvent.DRAG_END}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'dragend');
+ dispatchFirstElementEvent(canvas, 'dragend');
await fired;
});
@@ -135,7 +139,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`interval:${ChartEvent.DRAG}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'drag');
+ dispatchFirstElementEvent(canvas, 'drag');
await fired;
});
@@ -143,7 +147,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`interval:${ChartEvent.DRAG_ENTER}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'dragenter');
+ dispatchFirstElementEvent(canvas, 'dragenter');
await fired;
});
@@ -151,7 +155,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`interval:${ChartEvent.DRAG_LEAVE}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'dragleave');
+ dispatchFirstElementEvent(canvas, 'dragleave');
await fired;
});
@@ -159,7 +163,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`interval:${ChartEvent.DRAG_OVER}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'dragover');
+ dispatchFirstElementEvent(canvas, 'dragover');
await fired;
});
@@ -167,7 +171,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on(`interval:${ChartEvent.DROP}`, receiveExpectData(resolve));
- dispatchEvent(canvas, 'drop');
+ dispatchFirstElementEvent(canvas, 'drop');
await fired;
});
diff --git a/__tests__/integration/api-chart-on-series-element.spec.ts b/__tests__/integration/api-chart-on-series-element.spec.ts
index 372f4254e2..2a52ff77cc 100644
--- a/__tests__/integration/api-chart-on-series-element.spec.ts
+++ b/__tests__/integration/api-chart-on-series-element.spec.ts
@@ -1,6 +1,10 @@
import { chartOnSeriesElement as render } from '../plots/api/chart-on-series-element';
import { createDOMGCanvas } from './utils/createDOMGCanvas';
-import { dispatchEvent, createPromise, receiveExpectData } from './utils/event';
+import {
+ dispatchFirstElementEvent,
+ createPromise,
+ receiveExpectData,
+} from './utils/event';
import './utils/useSnapshotMatchers';
const data = {
@@ -77,7 +81,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on('element:click', receiveExpectData(resolve, data));
- dispatchEvent(canvas, 'click', { detail: 1 });
+ dispatchFirstElementEvent(canvas, 'click', { detail: 1 });
await fired;
});
@@ -85,7 +89,7 @@ describe('chart.on', () => {
await finished;
const [fired, resolve] = createPromise();
chart.on('line:click', receiveExpectData(resolve, data));
- dispatchEvent(canvas, 'click', { detail: 1 });
+ dispatchFirstElementEvent(canvas, 'click', { detail: 1 });
await fired;
});
diff --git a/__tests__/integration/snapshots/api/chart-emit-item-tooltip/step0.html b/__tests__/integration/snapshots/api/chart-emit-item-tooltip/step0.html
new file mode 100644
index 0000000000..f15473bf69
--- /dev/null
+++ b/__tests__/integration/snapshots/api/chart-emit-item-tooltip/step0.html
@@ -0,0 +1,46 @@
+
;
diff --git a/__tests__/integration/snapshots/api/chart-emit-item-tooltip/step1.html b/__tests__/integration/snapshots/api/chart-emit-item-tooltip/step1.html
new file mode 100644
index 0000000000..6e556feefa
--- /dev/null
+++ b/__tests__/integration/snapshots/api/chart-emit-item-tooltip/step1.html
@@ -0,0 +1,46 @@
+;
diff --git a/__tests__/integration/snapshots/api/chart-emit-pie-tooltip/step0.html b/__tests__/integration/snapshots/api/chart-emit-pie-tooltip/step0.html
new file mode 100644
index 0000000000..a6716d8807
--- /dev/null
+++ b/__tests__/integration/snapshots/api/chart-emit-pie-tooltip/step0.html
@@ -0,0 +1,40 @@
+;
diff --git a/__tests__/integration/snapshots/api/chart-emit-series-tooltip/step0.html b/__tests__/integration/snapshots/api/chart-emit-series-tooltip/step0.html
new file mode 100644
index 0000000000..ba61791da5
--- /dev/null
+++ b/__tests__/integration/snapshots/api/chart-emit-series-tooltip/step0.html
@@ -0,0 +1,46 @@
+;
diff --git a/__tests__/integration/snapshots/api/chart-emit-series-tooltip/step1.html b/__tests__/integration/snapshots/api/chart-emit-series-tooltip/step1.html
new file mode 100644
index 0000000000..0192756a69
--- /dev/null
+++ b/__tests__/integration/snapshots/api/chart-emit-series-tooltip/step1.html
@@ -0,0 +1,46 @@
+;
diff --git a/__tests__/integration/utils/event.ts b/__tests__/integration/utils/event.ts
index 55f0dd541c..d2cd94d972 100644
--- a/__tests__/integration/utils/event.ts
+++ b/__tests__/integration/utils/event.ts
@@ -14,14 +14,28 @@ export function receiveExpectData(
sold: 275,
},
},
+ nativeEvent = true,
+ asset = (event, data) => {
+ if (data === null) {
+ expect(event.data).toBeUndefined();
+ } else {
+ expect(event.data).toEqual(data);
+ }
+ },
) {
return (event) => {
- expect(event.data).toEqual(data);
+ asset(event, data);
+ expect(event.nativeEvent).toBe(nativeEvent);
resolve();
};
}
-export function dispatchEvent(canvas, event, params?) {
+export function dispatchFirstElementEvent(canvas, event, params?) {
const [element] = canvas.document.getElementsByClassName('element');
element.dispatchEvent(new CustomEvent(event, params));
}
+
+export function dispatchPlotEvent(canvas, event, params?) {
+ const [plot] = canvas.document.getElementsByClassName('plot');
+ plot.dispatchEvent(new CustomEvent(event, params));
+}
diff --git a/__tests__/plots/api/chart-emit-item-tooltip.ts b/__tests__/plots/api/chart-emit-item-tooltip.ts
new file mode 100644
index 0000000000..486ff03b21
--- /dev/null
+++ b/__tests__/plots/api/chart-emit-item-tooltip.ts
@@ -0,0 +1,68 @@
+import { Chart } from '../../../src';
+
+export function chartEmitItemTooltip(context) {
+ const { container, canvas } = context;
+
+ // wrapperDiv
+ const wrapperDiv = document.createElement('div');
+ container.appendChild(wrapperDiv);
+
+ // button
+ const button = document.createElement('button');
+ button.innerText = 'Hide tooltip';
+ container.appendChild(button);
+
+ // p
+ const p = document.createElement('p');
+ p.innerText = '';
+ container.appendChild(p);
+
+ const chart = new Chart({
+ theme: 'classic',
+ container: wrapperDiv,
+ canvas,
+ });
+
+ chart
+ .interval()
+ .data([
+ { genre: 'Sports', sold: 275 },
+ { genre: 'Strategy', sold: 115 },
+ { genre: 'Action', sold: 120 },
+ { genre: 'Shooter', sold: 350 },
+ { genre: 'Other', sold: 150 },
+ ])
+ .encode('x', 'genre')
+ .encode('y', 'sold')
+ .encode('color', 'genre');
+
+ const finished = chart.render();
+
+ finished.then((chart) =>
+ chart.emit('tooltip:show', {
+ data: { data: { sold: 115 } },
+ }),
+ );
+
+ chart.on('tooltip:show', ({ data }) => {
+ p.innerText = JSON.stringify(data);
+ });
+
+ const hide = () => {
+ console.log('hide');
+ };
+ chart.on('tooltip:hide', hide);
+
+ button.onclick = () => {
+ chart.emit('tooltip:hide');
+ };
+
+ return {
+ chart,
+ button,
+ finished,
+ clear: () => {
+ chart.off('tooltip:hide', hide);
+ },
+ };
+}
diff --git a/__tests__/plots/api/chart-emit-pie-tooltip.ts b/__tests__/plots/api/chart-emit-pie-tooltip.ts
new file mode 100644
index 0000000000..fb887aec3f
--- /dev/null
+++ b/__tests__/plots/api/chart-emit-pie-tooltip.ts
@@ -0,0 +1,69 @@
+import { Chart } from '../../../src';
+
+export function chartEmitPieTooltip(context) {
+ const { container, canvas } = context;
+
+ // wrapperDiv
+ const wrapperDiv = document.createElement('div');
+ container.appendChild(wrapperDiv);
+
+ // button
+ const button = document.createElement('button');
+ button.innerText = 'Hide tooltip';
+ container.appendChild(button);
+
+ // p
+ const p = document.createElement('p');
+ p.innerText = '';
+ container.appendChild(p);
+
+ const chart = new Chart({
+ theme: 'classic',
+ container: wrapperDiv,
+ canvas,
+ });
+
+ chart
+ .interval()
+ .data([
+ { genre: 'Sports', sold: 275 },
+ { genre: 'Strategy', sold: 115 },
+ { genre: 'Action', sold: 120 },
+ { genre: 'Shooter', sold: 350 },
+ { genre: 'Other', sold: 150 },
+ ])
+ .encode('y', 'sold')
+ .encode('color', 'genre')
+ .transform({ type: 'stackY' })
+ .coordinate({ type: 'theta' });
+
+ const finished = chart.render();
+
+ finished.then((chart) =>
+ chart.emit('tooltip:show', {
+ data: { data: { genre: 'Sports' } },
+ }),
+ );
+
+ chart.on('tooltip:show', ({ data }) => {
+ p.innerText = JSON.stringify(data);
+ });
+
+ const hide = () => {
+ console.log('hide');
+ };
+ chart.on('tooltip:hide', hide);
+
+ button.onclick = () => {
+ chart.emit('tooltip:hide');
+ };
+
+ return {
+ chart,
+ button,
+ finished,
+ clear: () => {
+ chart.off('tooltip:hide', hide);
+ },
+ };
+}
diff --git a/__tests__/plots/api/chart-emit-series-tooltip.ts b/__tests__/plots/api/chart-emit-series-tooltip.ts
new file mode 100644
index 0000000000..7aa4b13b34
--- /dev/null
+++ b/__tests__/plots/api/chart-emit-series-tooltip.ts
@@ -0,0 +1,62 @@
+import { Chart } from '../../../src';
+
+export function chartEmitSeriesTooltip(context) {
+ const { container, canvas } = context;
+
+ // wrapperDiv
+ const wrapperDiv = document.createElement('div');
+ container.appendChild(wrapperDiv);
+
+ // button
+ const button = document.createElement('button');
+ button.innerText = 'Hide tooltip';
+ container.appendChild(button);
+
+ // p
+ const p = document.createElement('p');
+ p.innerText = '';
+ container.appendChild(p);
+
+ const chart = new Chart({
+ theme: 'classic',
+ container: wrapperDiv,
+ canvas,
+ });
+
+ chart.options({
+ type: 'line',
+ data: { type: 'fetch', value: 'data/aapl.csv' },
+ encode: { x: 'date', y: 'close' },
+ tooltip: { title: (d) => new Date(d.date).toUTCString() },
+ });
+
+ const finished = chart.render();
+
+ finished.then((chart) =>
+ chart.emit('tooltip:show', {
+ data: { data: { x: new Date('2010-11-16') } },
+ }),
+ );
+
+ chart.on('tooltip:show', ({ data }) => {
+ p.innerText = JSON.stringify(data);
+ });
+
+ const hide = () => {
+ console.log('hide');
+ };
+ chart.on('tooltip:hide', hide);
+
+ button.onclick = () => {
+ chart.emit('tooltip:hide');
+ };
+
+ return {
+ chart,
+ button,
+ finished,
+ clear: () => {
+ chart.off('tooltip:hide', hide);
+ },
+ };
+}
diff --git a/__tests__/plots/api/index.ts b/__tests__/plots/api/index.ts
index b8bd16a0d2..cdff32ca93 100644
--- a/__tests__/plots/api/index.ts
+++ b/__tests__/plots/api/index.ts
@@ -17,3 +17,6 @@ export { chartRenderClearAnimation } from './chart-render-clear-animation';
export { chartOnBrushFilter } from './chart-on-brush-filter';
export { chartOptionsChangeData } from './chart-options-change-data';
export { chartOnFocusContext } from './chart-on-focus-context';
+export { chartEmitItemTooltip } from './chart-emit-item-tooltip';
+export { chartEmitSeriesTooltip } from './chart-emit-series-tooltip';
+export { chartEmitPieTooltip } from './chart-emit-pie-tooltip';
diff --git a/site/docs/spec/interaction/tooltip.zh.md b/site/docs/spec/interaction/tooltip.zh.md
index 783ec6d699..4ef031df1f 100644
--- a/site/docs/spec/interaction/tooltip.zh.md
+++ b/site/docs/spec/interaction/tooltip.zh.md
@@ -63,6 +63,8 @@ type TooltipPosition =
| 'bottom-right';
```
+## 案例
+
### 自定义 Tooltip
@@ -96,3 +98,68 @@ chart.interaction('tooltip', {
chart.render();
```
+
+## 获得提示数据
+
+```js
+chart.on('tooltip:show', (event) => {
+ console.log(event.data.data);
+});
+
+chart.on('tooltip:hide', () => {
+ console.log('hide');
+});
+```
+
+## 手动控制展示/隐藏
+
+对于 Interval、Point 等非系列 Mark,控制展示的方式如下:
+
+```js
+// 条形图、点图等
+chart
+ .interval()
+ .data([
+ { genre: 'Sports', sold: 275 },
+ { genre: 'Strategy', sold: 115 },
+ { genre: 'Action', sold: 120 },
+ { genre: 'Shooter', sold: 350 },
+ { genre: 'Other', sold: 150 },
+ ])
+ .encode('x', 'genre')
+ .encode('y', 'sold')
+ .encode('color', 'genre');
+
+chart.render().then((chart) =>
+ chart.emit('tooltip:show', {
+ data: {
+ // 会找从原始数据里面找到匹配的数据
+ data: { genre: 'Sports' },
+ },
+ }),
+);
+```
+
+对于 Line、Area 等系列 Mark,控制展示的方式如下:
+
+```js
+chart
+ .line()
+ .data({ type: 'fetch', value: 'data/aapl.csv' })
+ .encode('x', 'date')
+ .encode('y', 'close');
+
+chart.render((chart) =>
+ chart.emit('tooltip:show', {
+ data: {
+ data: { x: new Date('2010-11-16') },
+ },
+ }),
+);
+```
+
+隐藏的方式如下:
+
+```js
+chart.emit('tooltip:hide');
+```
diff --git a/src/interaction/brushFilter.ts b/src/interaction/brushFilter.ts
index d88d96e635..f21a942ac9 100644
--- a/src/interaction/brushFilter.ts
+++ b/src/interaction/brushFilter.ts
@@ -131,9 +131,10 @@ export function BrushFilter({ hideX = true, hideY = true, ...rest }) {
);
// Emit event.
- event.data = event.data || {};
- event.data.selection = [domainX, domainY];
- emitter.emit('brush:filter', event);
+ emitter.emit('brush:filter', {
+ ...event,
+ data: { selection: [domainX, domainY] },
+ });
// Rerender and update view.
const newOptions = {
@@ -154,9 +155,10 @@ export function BrushFilter({ hideX = true, hideY = true, ...rest }) {
const { x: scaleX, y: scaleY } = scale;
const domainX = scaleX.getOptions().domain;
const domainY = scaleY.getOptions().domain;
- event.data = event.data || {};
- event.data.selection = [domainX, domainY];
- emitter.emit('brush:filter', event);
+ emitter.emit('brush:filter', {
+ ...event,
+ data: { selection: [domainX, domainY] },
+ });
filtered = false;
newView = view;
diff --git a/src/interaction/event.ts b/src/interaction/event.ts
index b6c5a71fb0..6aa439ffa9 100644
--- a/src/interaction/event.ts
+++ b/src/interaction/event.ts
@@ -1,6 +1,6 @@
import { ChartEvent } from '../utils/event';
-function dataOf(element, view) {
+export function dataOf(element, view) {
const { __data__: datum } = element;
const { markKey, index, seriesIndex } = datum;
const { markState } = view;
@@ -14,21 +14,19 @@ function dataOf(element, view) {
return selectedMark.data[index];
}
-function updateData(event, target, view) {
- const { data = {} } = event;
- data.data = dataOf(target, view);
- event.data = data;
-}
-
function bubblesEvent(eventType, view, emitter, predicate = (event) => true) {
return (e) => {
if (!predicate(e)) return;
const { target } = e;
const { className: elementType, markType } = target;
if (elementType === 'element') {
- updateData(e, target, view);
- emitter.emit(`element:${eventType}`, e);
- emitter.emit(`${markType}:${eventType}`, e);
+ const e1 = {
+ ...e,
+ nativeEvent: true,
+ data: { data: dataOf(target, view) },
+ };
+ emitter.emit(`element:${eventType}`, e1);
+ emitter.emit(`${markType}:${eventType}`, e1);
return;
}
// @todo Handle click axis and legend.
diff --git a/src/interaction/tooltip.ts b/src/interaction/tooltip.ts
index 3dd43bb17d..fb0e57c27c 100644
--- a/src/interaction/tooltip.ts
+++ b/src/interaction/tooltip.ts
@@ -6,13 +6,16 @@ import { Constant, Identity } from '@antv/scale';
import { defined, subObject } from '../utils/helper';
import { isTranspose, isPolar } from '../utils/coordinate';
import { angle, sub } from '../utils/vector';
+import { invert } from '../utils/scale';
import {
selectG2Elements,
createXKey,
selectPlotArea,
mousePosition,
selectFacetG2Elements,
+ createDatumof,
} from './utils';
+import { dataOf } from './event';
function getContainer(group: IElement) {
// @ts-ignore
@@ -88,10 +91,15 @@ function showTooltip({
container.tooltipElement = tooltipElement;
}
-function hideTooltip(root, single) {
+function hideTooltip({ root, single, emitter, nativeEvent = true }) {
const container = single ? getContainer(root) : root;
const { tooltipElement } = container;
- if (tooltipElement) tooltipElement.hide();
+ if (tooltipElement) {
+ tooltipElement.hide();
+ if (nativeEvent) {
+ emitter.emit('tooltip:hide', { nativeEvent });
+ }
+ }
}
function destroyTooltip(root) {
@@ -300,6 +308,7 @@ export function seriesTooltip(
crosshairs,
render,
groupName,
+ emitter,
wait = 50,
leading = true,
trailing = false,
@@ -483,16 +492,41 @@ export function seriesTooltip(
polar,
});
}
+
+ emitter.emit('tooltip:show', {
+ ...event,
+ nativeEvent: true,
+ data: { data: { x: invert(scale.x, abstractX(focus), true) } },
+ });
},
wait,
{ leading, trailing },
) as (...args: any[]) => void;
const hide = () => {
- hideTooltip(root, single);
+ hideTooltip({ root, single, emitter });
if (crosshairs) hideRuleY(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 });
+ };
+
+ emitter.on('tooltip:show', onTooltipShow);
+ emitter.on('tooltip:hide', onTooltipHide);
+
root.addEventListener('pointerenter', update);
root.addEventListener('pointermove', update);
root.addEventListener('pointerleave', hide);
@@ -501,6 +535,8 @@ export function seriesTooltip(
root.removeEventListener('pointerenter', update);
root.removeEventListener('pointermove', update);
root.removeEventListener('pointerleave', hide);
+ emitter.off('tooltip:show', onTooltipShow);
+ emitter.off('tooltip:hide', onTooltipHide);
destroyTooltip(root);
if (crosshairs) hideRuleY(root);
};
@@ -518,6 +554,7 @@ export function tooltip(
groupName,
sort: sortFunction,
filter: filterFunction,
+ emitter,
wait = 50,
leading = true,
trailing = false,
@@ -525,6 +562,8 @@ export function tooltip(
single = true,
position,
enterable,
+ datum,
+ view,
}: Record,
) {
const elements = elementsof(root);
@@ -535,7 +574,7 @@ export function tooltip(
(event) => {
const { target: element } = event;
if (!elementSet.has(element)) {
- hideTooltip(root, single);
+ hideTooltip({ root, single, emitter });
return;
}
const k = groupKey(element);
@@ -554,7 +593,7 @@ export function tooltip(
}
if (isEmptyTooltipData(data)) {
- hideTooltip(root, single);
+ hideTooltip({ root, single, emitter });
return;
}
@@ -570,6 +609,14 @@ export function tooltip(
position,
enterable,
});
+
+ emitter.emit('tooltip:show', {
+ ...event,
+ nativeEvent: true,
+ data: {
+ data: dataOf(element, view),
+ },
+ });
},
wait,
{ leading, trailing },
@@ -578,9 +625,34 @@ export function tooltip(
const pointerout = (event) => {
const { target: element } = event;
if (!elementSet.has(element)) return;
- hideTooltip(root, single);
+ hideTooltip({ root, single, emitter });
+ };
+
+ const onTooltipShow = ({ nativeEvent, data }) => {
+ if (nativeEvent) return;
+ const element = elements.find((d) =>
+ Object.entries(data.data).every(
+ ([key, value]) => datum(d)[key] === value,
+ ),
+ );
+ 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 }: any = {}) => {
+ if (nativeEvent) return;
+ hideTooltip({ root, single, emitter, nativeEvent: false });
+ };
+
+ emitter.on('tooltip:show', onTooltipShow);
+ emitter.on('tooltip:hide', onTooltipHide);
+
root.addEventListener('pointerover', pointerover);
root.addEventListener('pointermove', pointerover);
root.addEventListener('pointerout', pointerout);
@@ -589,6 +661,8 @@ export function tooltip(
root.removeEventListener('pointerover', pointerover);
root.removeEventListener('pointermove', pointerover);
root.removeEventListener('pointerout', pointerout);
+ emitter.off('tooltip:show', onTooltipShow);
+ emitter.off('tooltip:hide', onTooltipHide);
destroyTooltip(root);
};
}
@@ -603,7 +677,7 @@ export function Tooltip(options) {
facet = false,
...rest
} = options;
- return (target, viewInstances) => {
+ return (target, viewInstances, emitter) => {
const { container, view } = target;
const { scale, markState, coordinate } = view;
// Get default value from mark states.
@@ -621,6 +695,7 @@ export function Tooltip(options) {
coordinate,
crosshairs: maybeValue(crosshairs, defaultShowCrosshairs),
item,
+ emitter,
});
}
@@ -649,16 +724,20 @@ export function Tooltip(options) {
item,
startX,
startY,
+ emitter,
});
}
return tooltip(plotArea, {
...rest,
+ datum: createDatumof(view),
elements: selectG2Elements,
scale,
coordinate,
groupKey: shared ? createXKey(view) : undefined,
item,
+ emitter,
+ view,
});
};
}