From 5f64f7165a14de0d66350fb7316654027b6bce7c Mon Sep 17 00:00:00 2001 From: MiniPear Date: Wed, 10 May 2023 13:33:28 +0800 Subject: [PATCH] feat(tooltip): on and emit (#4980) --- .../api-chart-emit-item-tooltip.spec.ts | 51 ++++++++++ .../api-chart-emit-pie-tooltip.spec.ts | 27 ++++++ .../api-chart-emit-series-tooltip.spec.ts | 59 ++++++++++++ .../api-chart-on-item-element.spec.ts | 44 +++++---- .../api-chart-on-series-element.spec.ts | 10 +- .../api/chart-emit-item-tooltip/step0.html | 46 +++++++++ .../api/chart-emit-item-tooltip/step1.html | 46 +++++++++ .../api/chart-emit-pie-tooltip/step0.html | 40 ++++++++ .../api/chart-emit-series-tooltip/step0.html | 46 +++++++++ .../api/chart-emit-series-tooltip/step1.html | 46 +++++++++ __tests__/integration/utils/event.ts | 18 +++- .../plots/api/chart-emit-item-tooltip.ts | 68 ++++++++++++++ __tests__/plots/api/chart-emit-pie-tooltip.ts | 69 ++++++++++++++ .../plots/api/chart-emit-series-tooltip.ts | 62 +++++++++++++ __tests__/plots/api/index.ts | 3 + site/docs/spec/interaction/tooltip.zh.md | 67 +++++++++++++ src/interaction/brushFilter.ts | 14 +-- src/interaction/event.ts | 18 ++-- src/interaction/tooltip.ts | 93 +++++++++++++++++-- 19 files changed, 779 insertions(+), 48 deletions(-) create mode 100644 __tests__/integration/api-chart-emit-item-tooltip.spec.ts create mode 100644 __tests__/integration/api-chart-emit-pie-tooltip.spec.ts create mode 100644 __tests__/integration/api-chart-emit-series-tooltip.spec.ts create mode 100644 __tests__/integration/snapshots/api/chart-emit-item-tooltip/step0.html create mode 100644 __tests__/integration/snapshots/api/chart-emit-item-tooltip/step1.html create mode 100644 __tests__/integration/snapshots/api/chart-emit-pie-tooltip/step0.html create mode 100644 __tests__/integration/snapshots/api/chart-emit-series-tooltip/step0.html create mode 100644 __tests__/integration/snapshots/api/chart-emit-series-tooltip/step1.html create mode 100644 __tests__/plots/api/chart-emit-item-tooltip.ts create mode 100644 __tests__/plots/api/chart-emit-pie-tooltip.ts create mode 100644 __tests__/plots/api/chart-emit-series-tooltip.ts 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 @@ +
+
+ Strategy +
+ +
; 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 @@ +
+
+ Tue, 16 Nov 2010 00:00:00 GMT +
+ +
; 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 custom-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, }); }; }