Skip to content

Commit

Permalink
feat(interaction): emit legend highlight (#5126)
Browse files Browse the repository at this point in the history
  • Loading branch information
pearmini committed May 30, 2023
1 parent 7b50766 commit e796355
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 14 deletions.
71 changes: 71 additions & 0 deletions __tests__/integration/api-chart-emit-legend-highlight.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { chartEmitLegendHighlight as render } from '../plots/api/chart-emit-legend-highlight';
import {
LEGEND_ITEMS_CLASS_NAME,
CATEGORY_LEGEND_CLASS_NAME,
} from '../../src/interaction/legendFilter';
import { createNodeGCanvas } from './utils/createNodeGCanvas';
import { sleep } from './utils/sleep';
import { kebabCase } from './utils/kebabCase';
import { createPromise, dispatchFirstShapeEvent } 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("legend:highlight") should receive expected data.', async () => {
const { chart, finished } = render({
canvas,
container: document.createElement('div'),
});
await finished;
await sleep(20);

// chart.emit('legend:highlight', options) should trigger slider.
chart.emit('legend:highlight', {
data: { channel: 'color', value: 'Increase' },
});
await sleep(20);
await expect(canvas).toMatchCanvasSnapshot(dir, 'step0');

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

chart.off();

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

// chart.on("legend:highlight") should receive expected data.
const [highlight, resolveHighlight] = createPromise();
chart.on('legend:highlight', (event) => {
if (!event.nativeEvent) return;
expect(event.data).toEqual({ channel: 'color', value: 'Increase' });
resolveHighlight();
});
dispatchFirstShapeEvent(canvas, LEGEND_ITEMS_CLASS_NAME, 'pointerover', {
nativeEvent: true,
});
await sleep(20);
await highlight;
});

afterAll(() => {
canvas?.destroy();
});
});
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.
5 changes: 5 additions & 0 deletions __tests__/integration/utils/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,8 @@ export function dispatchPlotEvent(canvas, event, params?) {
const [plot] = canvas.document.getElementsByClassName('plot');
plot.dispatchEvent(new CustomEvent(event, params));
}

export function dispatchFirstShapeEvent(canvas, className, event, params?) {
const [shape] = canvas.document.getElementsByClassName(className);
shape.dispatchEvent(new CustomEvent(event, params));
}
72 changes: 72 additions & 0 deletions __tests__/plots/api/chart-emit-legend-highlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Chart } from '../../../src';
import { profit } from '../../data/profit';

export function chartEmitLegendHighlight(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,
canvas,
});

chart.options({
paddingLeft: 60,
type: 'interval',
data: profit,
axis: { y: { labelFormatter: '~s' } },
encode: {
x: 'month',
y: ['end', 'start'],
color: (d) =>
d.month === 'Total' ? 'Total' : d.profit > 0 ? 'Increase' : 'Decrease',
},
state: { inactive: { opacity: 0.5 } },
legend: {
color: { state: { inactive: { labelOpacity: 0.5, markerOpacity: 0.5 } } },
},
interaction: {
legendHighlight: true,
tooltip: false,
},
});

chart.on('legend:highlight', (e) => {
const { nativeEvent, data } = e;
if (!nativeEvent) return;
console.log(data);
});

chart.on('legend:unhighlight', (e) => {
const { nativeEvent } = e;
if (!nativeEvent) return;
console.log('unhighlight');
});

button.onclick = () => {
chart.emit('legend:highlight', {
data: { channel: 'color', value: 'Increase' },
});
};

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

const finished = chart.render();

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 @@ -29,3 +29,4 @@ export { chartEmitSliderFilter } from './chart-emit-slider-filter';
export { chartEmitElementHighlight } from './chart-emit-element-highlight';
export { chartEmitElementSelect } from './chart-emit-element-select';
export { chartEmitElementSelectSingle } from './chart-emit-element-select-single';
export { chartEmitLegendHighlight } from './chart-emit-legend-highlight';
28 changes: 28 additions & 0 deletions site/docs/spec/interaction/legendHighlight.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,31 @@ chart.interaction('legendHighlight', true);

chart.render();
```

## 妗堜緥

### 鑾峰緱鏁版嵁

```js
chart.on('legend:highlight', (e) => {
const { nativeEvent, data } = e;
if (!nativeEvent) return;
console.log(data);
});

chart.on('legend:unhighlight', (e) => {
const { nativeEvent } = e;
if (!nativeEvent) return;
console.log('unhighlight');
});
```

### 瑙﹀彂浜や簰

```js
chart.emit('legend:highlight', {
data: { channel: 'color', value: 'Increase' },
});

chart.emit('legend:unhighlight', {});
```
69 changes: 55 additions & 14 deletions src/interaction/legendHighlight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { markerOf, labelOf, itemsOf, legendsOf, dataOf } from './legendFilter';

export function LegendHighlight() {
return (context) => {
return (context, _, emitter) => {
const { container, view, options } = context;
const legends = legendsOf(container);
const elements = selectG2Elements(container);
Expand All @@ -25,6 +25,7 @@ export function LegendHighlight() {
};
const markState = mergeState(options, ['active', 'inactive']);
const valueof = createValueof(elements, createDatumof(view));
const destroys = [];

// Bind events for each legend.
for (const legend of legends) {
Expand Down Expand Up @@ -62,39 +63,79 @@ export function LegendHighlight() {
}
}
};
const highlightItem = (event, item) => {
// Update UI.
const value = datumOf(item);
const elementSet = new Set(elementGroup.get(value));
for (const e of elements) {
if (elementSet.has(e)) setState(e, 'active');
else setState(e, 'inactive');
}
updateLegendState(item);

// Emit events.
const { nativeEvent = true } = event;
if (!nativeEvent) return;
emitter.emit('legend:highlight', {
...event,
nativeEvent,
data: { channel, value },
});
};

const itemPointerover = new Map();

// Add listener for the legend items.
for (const item of items) {
const pointerover = () => {
const value = datumOf(item);
const elementSet = new Set(elementGroup.get(value));
for (const e of elements) {
if (elementSet.has(e)) setState(e, 'active');
else setState(e, 'inactive');
}
updateLegendState(item);
const pointerover = (event) => {
highlightItem(event, item);
};
item.addEventListener('pointerover', pointerover);
itemPointerover.set(item, pointerover);
}

// Add listener for the legend group.
const pointerleave = () => {
for (const e of elements) {
removeState(e, 'inactive', 'active');
}
const pointerleave = (event) => {
for (const e of elements) removeState(e, 'inactive', 'active');
updateLegendState(null);

// Emit events.
const { nativeEvent = true } = event;
if (!nativeEvent) return;
emitter.emit('legend:unhighlight', { nativeEvent });
};

const onHighlight = (event) => {
const { nativeEvent, data } = event;
if (nativeEvent) return;
const { channel: specifiedChannel, value } = data;
if (specifiedChannel !== channel) return;
const item = items.find((d) => datumOf(d) === value);
if (!item) return;
highlightItem({ nativeEvent: false }, item);
};

const onUnHighlight = (event) => {
const { nativeEvent } = event;
if (nativeEvent) return;
pointerleave({ nativeEvent: false });
};

legend.addEventListener('pointerleave', pointerleave);
emitter.on('legend:highlight', onHighlight);
emitter.on('legend:unhighlight', onUnHighlight);

return () => {
const destroy = () => {
legend.removeEventListener(pointerleave);
emitter.off('legend:highlight', onHighlight);
emitter.off('legend:unhighlight', onUnHighlight);
for (const [item, pointerover] of itemPointerover) {
item.removeEventListener(pointerover);
}
};
destroys.push(destroy);
}

return () => destroys.forEach((d) => d());
};
}

0 comments on commit e796355

Please sign in to comment.