Skip to content

Commit

Permalink
feat(interaction): emit element highlight (#5117)
Browse files Browse the repository at this point in the history
  • Loading branch information
pearmini committed May 30, 2023
1 parent 32ac0f6 commit 44b8ee5
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 15 deletions.
75 changes: 75 additions & 0 deletions __tests__/integration/api-chart-emit-element-highlight.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { chartEmitElementHighlight as render } from '../plots/api/chart-emit-element-highlight';
import { createNodeGCanvas } from './utils/createNodeGCanvas';
import { sleep } from './utils/sleep';
import { kebabCase } from './utils/kebabCase';
import { createPromise, dispatchFirstElementEvent } from './utils/event';
import './utils/useSnapshotMatchers';
import './utils/useCustomFetch';

describe('chart.emit', () => {
const dir = `${__dirname}/snapshots/api/${kebabCase(render.name)}`;
const canvas = createNodeGCanvas(800, 500);

it('chart.on("element:highlight") should receive expected data.', async () => {
const { chart, finished } = render({
canvas,
container: document.createElement('div'),
});
await finished;
await sleep(20);

// chart.emit('element:highlight', options) should trigger slider.
chart.emit('element:highlight', {
data: { data: { population: 5038433 } },
});
await sleep(20);
await expect(canvas).toMatchCanvasSnapshot(dir, 'step0');

// chart.emit('element:unhighlight', options) should reset.
chart.emit('element:unhighlight', {});
await sleep(20);
await expect(canvas).toMatchCanvasSnapshot(dir, 'step1');

chart.off();

// chart.on("element:highlight") should receive expected data.
const [highlight, resolveHighlight] = createPromise();
chart.on('element:highlight', (event) => {
if (!event.nativeEvent) return;
expect(event.data.data).toEqual({
age: '<10',
population: 5038433,
state: 'CA',
});
expect(event.data.group).toEqual([
{ age: '<10', population: 5038433, state: 'CA' },
{ age: '10-19', population: 5170341, state: 'CA' },
{ age: '20-29', population: 5809455, state: 'CA' },
{ age: '30-39', population: 5354112, state: 'CA' },
{ age: '40-49', population: 5179258, state: 'CA' },
{ age: '50-59', population: 5042094, state: 'CA' },
{ age: '60-69', population: 3737461, state: 'CA' },
{ age: '70-79', population: 2011678, state: 'CA' },
{ age: '≥80', population: 1311374, state: 'CA' },
]);
resolveHighlight();
});
dispatchFirstElementEvent(canvas, 'pointerover');
await sleep(20);
await highlight;

// chart.on("element:unhighlight") should be called.
const [unhighlight, resolveUnhighlight] = createPromise();
chart.on('element:unhighlight', (event) => {
if (!event.nativeEvent) return;
resolveUnhighlight();
});
dispatchFirstElementEvent(canvas, 'pointerout');
await sleep(20);
await unhighlight;
});

afterAll(() => {
canvas?.destroy();
});
});
10 changes: 5 additions & 5 deletions __tests__/integration/api-chart-emit-slider-filter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { chartEmitSliderFilter as render } from '../plots/api/chart-emit-slider-filter';
import { SLIDER_CLASS_NAME } from '../../src/interaction/sliderFilter';
import { dispatchValueChange } from '../plots/tooltip/appl-line-slider-filter';
import { createNodeGCanvas } from './utils/createNodeGCanvas';
import { sleep } from './utils/sleep';
import { kebabCase } from './utils/kebabCase';
import './utils/useSnapshotMatchers';
import { SLIDER_CLASS_NAME } from '../../src/interaction/sliderFilter';
import { dispatchValueChange } from '../plots/tooltip/appl-line-slider-filter';
import { createPromise } from './utils/event';
import './utils/useSnapshotMatchers';

describe('chart.emit', () => {
const dir = `${__dirname}/snapshots/api/${kebabCase(render.name)}`;
Expand All @@ -28,15 +28,15 @@ describe('chart.emit', () => {
chart.emit('sliderX:filter', {
data: { selection: [X, undefined] },
});
await sleep(100);
await sleep(20);
await expect(canvas).toMatchCanvasSnapshot(dir, 'step0');

// chart.emit('sliderY:filter', options) should trigger slider.
const Y = [50, 550];
chart.emit('sliderY:filter', {
data: { selection: [undefined, Y] },
});
await sleep(100);
await sleep(20);
await expect(canvas).toMatchCanvasSnapshot(dir, 'step1');

chart.off();
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 74 additions & 0 deletions __tests__/plots/api/chart-emit-element-highlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Chart } from '../../../src';

export function chartEmitElementHighlight(context) {
const { container, canvas } = context;

// button
const button = document.createElement('button');
button.innerText = 'Highlight';
container.appendChild(button);

const button1 = document.createElement('button');
button1.innerText = 'reset';
container.appendChild(button1);

// wrapperDiv
const wrapperDiv = document.createElement('div');
container.appendChild(wrapperDiv);

const chart = new Chart({
theme: 'classic',
container: wrapperDiv,
padding: 'auto',
canvas,
});

chart.options({
type: 'interval',
transform: [
{ type: 'sortX', by: 'y', reverse: true, reducer: 'sum', slice: 6 },
{ type: 'dodgeX' },
],
data: {
type: 'fetch',
value: 'data/stateages.csv',
},
encode: {
x: 'state',
y: 'population',
color: 'age',
},
state: {
active: { fill: 'red' },
inactive: { opacity: 0.6 },
},
interaction: {
elementHighlightByX: { delay: 0 },
tooltip: false,
},
});

const finished = chart.render();

chart.on('element:highlight', (event) => {
const { data, nativeEvent } = event;
if (nativeEvent) console.log('element:highlight', data);
});

chart.on('element:unhighlight', (event) => {
const { nativeEvent } = event;
if (nativeEvent) console.log('reset');
});

button.onclick = () => {
chart.emit('element:highlight', {
data: { data: { population: 5038433 } },
});
};

button1.onclick = () => {
chart.emit('element:unhighlight', {});
};

return { chart, finished };
}
1 change: 1 addition & 0 deletions __tests__/plots/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export { chartEmitBrushHighlightX } from './chart-emit-brush-highlight-x';
export { chartRenderBrushEnd } from './chart-render-brush-end';
export { chartChangeDataEmpty } from './chart-change-data-empty';
export { chartEmitSliderFilter } from './chart-emit-slider-filter';
export { chartEmitElementHighlight } from './chart-emit-element-highlight';
26 changes: 26 additions & 0 deletions site/docs/spec/interaction/elementHighlight.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,29 @@ chart.render();
| background | 是否高亮背景 | `boolean` | false |
| offset | 主方向的偏移量 | `number` | 0 |
| `background${StyleAttrs}` | 背景的样式 | `StyleAttrs` | - |

## 案例

### 触发事件

```js
chart.emit('element:highlight', {
data: { data: { population: 5038433 } },
});

chart.emit('element:unhighlight', {});
```

### 获得数据

```js
chart.on('element:highlight', (event) => {
const { data, nativeEvent } = event;
if (nativeEvent) console.log('element:highlight', data);
});

chart.on('element:unhighlight', (event) => {
const { nativeEvent } = event;
if (nativeEvent) console.log('reset');
});
```
4 changes: 2 additions & 2 deletions site/docs/spec/interaction/tooltip.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ chart.interaction('tooltip', {
chart.render();
```

## 获得提示数据
### 获得提示数据

```js
chart.on('tooltip:show', (event) => {
Expand All @@ -114,7 +114,7 @@ chart.on('tooltip:hide', () => {
});
```

## 手动控制展示/隐藏
### 手动控制展示/隐藏

对于 Interval、Point 等非系列 Mark,控制展示的方式如下:

Expand Down
42 changes: 39 additions & 3 deletions src/interaction/elementHighlight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
offsetTransform,
renderBackground,
renderLink,
selectElementByData,
selectG2Elements,
selectPlotArea,
useState,
Expand All @@ -28,6 +29,7 @@ export function elementHighlight(
delay = 60, // delay to unhighlighted element
scale,
coordinate,
emitter,
state = {},
}: Record<string, any>,
) {
Expand Down Expand Up @@ -67,7 +69,7 @@ export function elementHighlight(

let out; // Timer for delaying unhighlighted.
const pointerover = (event) => {
const { target: element } = event;
const { target: element, nativeEvent = true } = event;
if (!elementSet.has(element)) return;
if (out) clearTimeout(out);
const k = groupKey(element);
Expand All @@ -84,6 +86,16 @@ export function elementHighlight(
}
appendBackground(element);
appendLink(group);

// Emit events.
if (!nativeEvent) return;
emitter.emit('element:highlight', {
nativeEvent,
data: {
data: datum(element),
group: group.map(datum),
},
});
};

const delayUnhighlighted = () => {
Expand All @@ -94,12 +106,15 @@ export function elementHighlight(
}, delay);
};

const unhighlighted = () => {
const unhighlighted = (nativeEvent = true) => {
for (const e of elements) {
removeState(e, 'active', 'inactive');
removeBackground(e);
removeLink(e);
}
if (nativeEvent) {
emitter.emit('element:unhighlight', { nativeEvent });
}
};

const pointerout = (event) => {
Expand All @@ -118,10 +133,30 @@ export function elementHighlight(
root.addEventListener('pointerout', pointerout);
root.addEventListener('pointerleave', pointerleave);

const onRest = (e) => {
const { nativeEvent } = e;
if (nativeEvent) return;
unhighlighted(false);
};

const onHighlight = (e) => {
const { nativeEvent } = e;
if (nativeEvent) return;
const { data } = e.data;
const element = selectElementByData(elements, data, datum);
if (!element) return;
pointerover({ target: element, nativeEvent: false });
};

emitter.on('element:highlight', onHighlight);
emitter.on('element:unhighlight', onRest);

return () => {
root.removeEventListener('pointerover', pointerover);
root.removeEventListener('pointerout', pointerout);
root.removeEventListener('pointerleave', pointerleave);
emitter.off('element:highlight', onHighlight);
emitter.off('element:unhighlight', onRest);
for (const e of elements) {
removeBackground(e);
removeLink(e);
Expand All @@ -136,7 +171,7 @@ export function ElementHighlight({
link = false,
...rest
}) {
return (context) => {
return (context, _, emitter) => {
const { container, view, options } = context;
const { scale, coordinate } = view;
const plotArea = selectPlotArea(container);
Expand All @@ -153,6 +188,7 @@ export function ElementHighlight({
background,
link,
delay,
emitter,
...rest,
});
};
Expand Down
7 changes: 2 additions & 5 deletions src/interaction/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
mousePosition,
selectFacetG2Elements,
createDatumof,
selectElementByData,
} from './utils';
import { dataOf } from './event';

Expand Down Expand Up @@ -683,11 +684,7 @@ export function tooltip(

const onTooltipShow = ({ nativeEvent, data }) => {
if (nativeEvent) return;
const element = elements.find((d) =>
Object.entries(data.data).every(
([key, value]) => datum(d)[key] === value,
),
);
const element = selectElementByData(elements, data.data, datum);
if (!element) return;
const bbox = element.getBBox();
const { x, y, width, height } = bbox;
Expand Down
6 changes: 6 additions & 0 deletions src/interaction/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,3 +456,9 @@ export function setCursor(root, cursor) {
export function restoreCursor(root) {
setCursor(root, 'default');
}

export function selectElementByData(elements, data, datum) {
return elements.find((d) =>
Object.entries(data).every(([key, value]) => datum(d)[key] === value),
);
}

0 comments on commit 44b8ee5

Please sign in to comment.