diff --git a/__tests__/integration/api-chart-on-component-click.spec.ts b/__tests__/integration/api-chart-on-component-click.spec.ts new file mode 100644 index 0000000000..80baa1f499 --- /dev/null +++ b/__tests__/integration/api-chart-on-component-click.spec.ts @@ -0,0 +1,72 @@ +import { chartOnComponentClick as render } from '../plots/api/chart-on-component-click'; +import { createDOMGCanvas } from './utils/createDOMGCanvas'; +import { dispatchFirstShapeEvent, createPromise } from './utils/event'; +import './utils/useSnapshotMatchers'; +import { ChartEvent } from '../../src'; + +describe('chart.on', () => { + const canvas = createDOMGCanvas(640, 480); + const { finished, chart } = render({ canvas }); + + chart.off(); + + it('chart.on("component:click", callback) should emit events', async () => { + await finished; + const [fired, resolve] = createPromise(); + chart.on(`component:${ChartEvent.CLICK}`, resolve); + dispatchFirstShapeEvent(canvas, 'component', 'click', { detail: 1 }); + await fired; + }); + + it('chart.on("legend-category:click", callback) should emit events', async () => { + await finished; + const [fired, resolve] = createPromise(); + chart.on(`legend-category:${ChartEvent.CLICK}`, resolve); + dispatchFirstShapeEvent(canvas, 'legend-category', 'click', { detail: 1 }); + await fired; + }); + + it('chart.on("legend-category-item-marker:click", callback) should emit events', async () => { + await finished; + const [fired, resolve] = createPromise(); + chart.on(`legend-category-item-marker:${ChartEvent.CLICK}`, resolve); + dispatchFirstShapeEvent(canvas, 'legend-category-item-marker', 'click', { detail: 1 }); + await fired; + }); + + it('chart.on("legend-category-item-label:click", callback) should emit events', async () => { + await finished; + const [fired, resolve] = createPromise(); + chart.on(`legend-category-item-label:${ChartEvent.CLICK}`, resolve); + dispatchFirstShapeEvent(canvas, 'legend-category-item-label', 'click', { detail: 1 }); + await fired; + }); + + it('chart.on("grid-line:click", callback) should emit events', async () => { + await finished; + const [fired, resolve] = createPromise(); + chart.on(`grid-line:${ChartEvent.CLICK}`, resolve); + dispatchFirstShapeEvent(canvas, 'grid-line', 'click', { detail: 1 }); + await fired; + }); + + it('chart.on("axis-tick-item:click", callback) should emit events', async () => { + await finished; + const [fired, resolve] = createPromise(); + chart.on(`axis-tick-item:${ChartEvent.CLICK}`, resolve); + dispatchFirstShapeEvent(canvas, 'axis-tick-item', 'click', { detail: 1 }); + await fired; + }); + + it('chart.on("axis-label-item:click", callback) should emit events', async () => { + await finished; + const [fired, resolve] = createPromise(); + chart.on(`axis-label-item:${ChartEvent.CLICK}`, resolve); + dispatchFirstShapeEvent(canvas, 'axis-label-item', 'click', { detail: 1 }); + await fired; + }); + + afterAll(() => { + canvas?.destroy(); + }); +}); diff --git a/__tests__/plots/api/chart-on-component-click.ts b/__tests__/plots/api/chart-on-component-click.ts new file mode 100644 index 0000000000..ad1999836a --- /dev/null +++ b/__tests__/plots/api/chart-on-component-click.ts @@ -0,0 +1,57 @@ +import { Chart } from '../../../src'; + +const componentNames = [ + // legend group + 'legend-category', + // legend item marker + 'legend-category-item-marker', + // legend item label + 'legend-category-item-label', + // axis + 'grid-line', + // tick + 'axis-tick-item', + // axis label + 'axis-label-item' +] + +export function chartOnComponentClick(context) { + const { container, canvas } = context; + + const chart = new Chart({ theme: 'classic', container, canvas }); + + chart + .interval() + .data([ + { month: 'Jan.', profit: 387264, start: 0, end: 387264 }, + { month: 'Feb.', profit: 772096, start: 387264, end: 1159360 }, + { month: 'Mar.', profit: 638075, start: 1159360, end: 1797435 }, + { month: 'Apr.', profit: -211386, start: 1797435, end: 1586049 }, + { month: 'May', profit: -138135, start: 1586049, end: 1447914 }, + { month: 'Jun', profit: -267238, start: 1447914, end: 1180676 }, + { month: 'Jul.', profit: 431406, start: 1180676, end: 1612082 }, + { month: 'Aug.', profit: 363018, start: 1612082, end: 1975100 }, + { month: 'Sep.', profit: -224638, start: 1975100, end: 1750462 }, + { month: 'Oct.', profit: -299867, start: 1750462, end: 1450595 }, + { month: 'Nov.', profit: 607365, start: 1450595, end: 2057960 }, + { month: 'Dec.', profit: 1106986, start: 2057960, end: 3164946 }, + { month: 'Total', start: 0, end: 3164946 }, + ]) + .encode('x', 'month') + .encode('y', ['end', 'start']) + .encode('color', (d) => + d.month === 'Total' ? 'Total' : d.profit > 0 ? 'Increase' : 'Decrease', + ) + .axis('y', { labelFormatter: '~s' }) + .tooltip(['start', 'end']); + + chart.on('component:click', () => console.log('click component')); + + componentNames.forEach(name => { + chart.on(`${name}:click`, () => console.log(`click ${name}`)); + }) + + const finished = chart.render(); + + return { chart, finished }; +} diff --git a/__tests__/plots/api/index.ts b/__tests__/plots/api/index.ts index e8d7271902..42f1be5096 100644 --- a/__tests__/plots/api/index.ts +++ b/__tests__/plots/api/index.ts @@ -40,6 +40,7 @@ export { chartEmitClickTooltip } from './chart-emit-click-tooltip'; export { chartChangeDataLegend } from './chart-change-data-legend'; export { chartTooltipMultiChart } from './chart-tooltip-multi-chart'; export { chartOnTextClick } from './chart-on-text-click'; +export { chartOnComponentClick } from './chart-on-component-click'; export { chartRenderEvent } from './chart-render-event'; export { chartRender3dScatterPlot } from './chart-render-3d-scatter-plot'; export { chartRender3dScatterPlotPerspective } from './chart-render-3d-scatter-plot-perspective'; diff --git a/site/docs/manual/extra-topics/event.zh.md b/site/docs/manual/extra-topics/event.zh.md index 3eead245e9..3a223a86b9 100644 --- a/site/docs/manual/extra-topics/event.zh.md +++ b/site/docs/manual/extra-topics/event.zh.md @@ -106,6 +106,13 @@ chart.on(`interval:${ChartEvent.CLICK}`, (ev) => { chart.on('plot:click', (event) => console.log(event)); ``` + +- 监听全局 component 事件 + +```js +chart.on('component:click', (event) => console.log(event)); +``` + ### 点击事件 | 事件名 | 说明 | 回调参数 | diff --git a/src/interaction/event.ts b/src/interaction/event.ts index ade757f11d..bdc903cdd8 100644 --- a/src/interaction/event.ts +++ b/src/interaction/event.ts @@ -14,14 +14,23 @@ export function dataOf(element, view) { return selectedMark.data[index]; } -// For extended shape. -function maybeElementRoot(node) { - if (node.className === 'element') return node; +function maybeRoot(node, rootOf) { + if (rootOf(node)) return node; let root = node.parent; - while (root && root.className !== 'element') root = root.parent; + while (root && !rootOf(root)) root = root.parent; return root; } +// For extended component +function maybeComponentRoot(node) { + return maybeRoot(node, node => node.className === 'component'); +} + +// For extended shape. +function maybeElementRoot(node) { + return maybeRoot(node, node => node.className === 'element'); +} + function bubblesEvent(eventType, view, emitter, predicate = (event) => true) { return (e) => { if (!predicate(e)) return; @@ -41,19 +50,23 @@ function bubblesEvent(eventType, view, emitter, predicate = (event) => true) { // If target is element or child of element. const elementRoot = maybeElementRoot(target); - if (!elementRoot) return; - const { className: elementType, markType } = elementRoot; + // If target is component or child of component. + const componentRoot = maybeComponentRoot(target); + const root = elementRoot || componentRoot; + if (!root) return; + const { className: elementType, markType } = root; if (elementType === 'element') { const e1 = { ...e, nativeEvent: true, - data: { data: dataOf(elementRoot, view) }, + data: { data: dataOf(root, view) }, }; emitter.emit(`element:${eventType}`, e1); emitter.emit(`${markType}:${eventType}`, e1); } else { - // @todo Handle click axis and legend. - emitter.emit(`${elementType}:${eventType}`, e); + const e1 = {...e, nativeEvent: true }; + emitter.emit(`component:${eventType}`, e1); + emitter.emit(`${className}:${eventType}`, e1); } }; }