Skip to content

Commit

Permalink
feat(interaction): emit legend filter (#5127)
Browse files Browse the repository at this point in the history
  • Loading branch information
pearmini authored May 30, 2023
1 parent e796355 commit b7071ef
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 83 deletions.
56 changes: 48 additions & 8 deletions __tests__/integration/api-chart-emit-legend-filter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,68 @@
import { chartEmitLegendFilter as render } from '../plots/api/chart-emit-legend-filter';
import { LEGEND_ITEMS_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);
let chart;

it('chart.emit("legend:filter", options) should filter channel', async () => {
const values = render({
it('chart.on("legend:filter") should receive expected data.', async () => {
const { chart, finished } = render({
canvas,
container: document.createElement('div'),
});
chart = values.chart;
await values.finished;
await finished;
await sleep(20);

// Click legend item.
const [item] = values.items;
item.dispatchEvent(new CustomEvent('click'));
// chart.emit('legend:filter', options) should trigger slider.
chart.emit('legend:filter', {
data: { channel: 'color', values: ['Sports', 'Strategy'] },
});
await sleep(20);
await expect(canvas).toMatchCanvasSnapshot(dir, 'step0');

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

chart.off();

// chart.on("legend:reset") should be called.
const [end, resolveEnd] = createPromise();
chart.on('legend:reset', (event) => {
if (!event.nativeEvent) return;
resolveEnd();
});
dispatchFirstShapeEvent(canvas, LEGEND_ITEMS_CLASS_NAME, 'click', {
nativeEvent: true,
});
dispatchFirstShapeEvent(canvas, LEGEND_ITEMS_CLASS_NAME, 'click', {
nativeEvent: true,
});
await sleep(20);
await end;

// chart.on("legend:filter") should receive expected data.
const [filter, resolveHighlight] = createPromise();
chart.on('legend:filter', (event) => {
if (!event.nativeEvent) return;
expect(event.data).toEqual({
channel: 'color',
values: ['Strategy', 'Action', 'Shooter', 'Other'],
});
resolveHighlight();
});
dispatchFirstShapeEvent(canvas, LEGEND_ITEMS_CLASS_NAME, 'click', {
nativeEvent: true,
});
await sleep(20);
await filter;
});

afterAll(() => {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 26 additions & 49 deletions __tests__/plots/api/chart-emit-legend-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ export function chartEmitLegendFilter(context) {
const { container, canvas } = context;

// button
const legend = document.createElement('div');
container.appendChild(legend);
const button = document.createElement('button');
button.innerText = 'filter';
container.appendChild(button);

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

// wrapperDiv
const wrapperDiv = document.createElement('div');
Expand All @@ -29,59 +34,31 @@ export function chartEmitLegendFilter(context) {
.encode('x', 'genre')
.encode('y', 'sold')
.encode('color', 'genre')
.animate(false)
.legend(false);
.animate(false);

const finished = chart.render();
const assetValues: any = {
chart,
finished,
};

finished.then(() => {
const scale = chart.getScaleByChannel('color');
const { domain, range } = scale.getOptions();
const excludedValues: any[] = [];

assetValues.items = domain.map((text, i) => {
const span = document.createElement('span');
const color = range[i];

// Items' style.
span.innerText = text;
span.style.display = 'inline-block';
span.style.padding = '0.5em';
span.style.color = color;
span.style.cursor = 'pointer';
chart.on('legend:filter', (e) => {
const { nativeEvent, data } = e;
if (!nativeEvent) return;
console.log(data);
});

span.onclick = () => {
const index = excludedValues.findIndex((d) => d === text);
if (index === -1) {
excludedValues.push(text);
span.style.color = '#aaa';
} else {
excludedValues.splice(index, 1);
span.style.color = color;
}
onChange(excludedValues);
};
chart.on('legend:reset', (e) => {
const { nativeEvent } = e;
if (!nativeEvent) return;
console.log('end');
});

return span;
button.onclick = () => {
chart.emit('legend:filter', {
data: { channel: 'color', values: ['Sports', 'Strategy'] },
});
};

// Mount items.
for (const item of assetValues.items) legend.append(item);

function onChange(values: any[]) {
const selectedValues = domain.filter((d) => !values.includes(d));

// Emit Event.
chart.emit('legend:filter', {
channel: 'color',
values: selectedValues,
});
}
});
button1.onclick = () => {
chart.emit('legend:reset', {});
};

return assetValues;
return { chart, finished };
}
28 changes: 28 additions & 0 deletions site/docs/spec/interaction/legendFilter.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,31 @@ chart.interaction('legendFilter', true);

chart.render();
```

## 案例

### 触发交互

```js
chart.emit('legend:filter', {
data: { channel: 'color', values: ['Sports', 'Strategy'] },
});

chart.emit('legend:reset', {});
```

### 获得数据

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

chart.on('legend:reset', (e) => {
const { nativeEvent } = e;
if (!nativeEvent) return;
console.log('end');
});
```
30 changes: 17 additions & 13 deletions site/examples/component/legend/demo/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@ const chart = new Chart({
container: 'container',
});

const data = [
{ genre: 'Sports', sold: 275 },
{ genre: 'Strategy', sold: 115 },
{ genre: 'Action', sold: 120 },
{ genre: 'Shooter', sold: 350 },
{ genre: 'Other', sold: 150 },
];

const colorField = 'genre';

chart
.interval()
.data([
{ genre: 'Sports', sold: 275 },
{ genre: 'Strategy', sold: 115 },
{ genre: 'Action', sold: 120 },
{ genre: 'Shooter', sold: 350 },
{ genre: 'Other', sold: 150 },
])
.data(data)
.encode('x', 'genre')
.encode('y', 'sold')
.encode('color', 'genre')
.encode('color', colorField)
.legend(false); // Hide built-in legends.

chart.render().then(renderCustomLegend);
Expand Down Expand Up @@ -62,11 +66,11 @@ function renderCustomLegend(chart) {
for (const item of items) legend.append(item);

// Emit legendFilter event.
function onChange(values: any[]) {
function onChange(values) {
const selectedValues = domain.filter((d) => !values.includes(d));
chart.emit('legend:filter', {
channel: 'color',
values: selectedValues,
});
const selectedData = data.filter((d) =>
selectedValues.includes(d[colorField]),
);
chart.changeData(selectedData);
}
}
59 changes: 46 additions & 13 deletions src/interaction/legendFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ function legendFilter(
label: labelOf, // given the legend returns the label
datum, // given the legend returns the value
filter, // invoke when dispatch filter event,
emitter,
channel,
state = {} as Record<string, any>, // state options
},
) {
Expand Down Expand Up @@ -79,7 +81,7 @@ function legendFilter(
);

const items: DisplayObject[] = Array.from(legends(root));
const selectedValues = items.map(datum);
let selectedValues = items.map(datum);
const updateLegendState = () => {
for (const item of items) {
const value = datum(item);
Expand All @@ -105,14 +107,30 @@ function legendFilter(
restoreCursor(root);
};

const click = async () => {
const click = async (event) => {
const value = datum(item);
const index = selectedValues.indexOf(value);
if (index === -1) selectedValues.push(value);
else selectedValues.splice(index, 1);
if (selectedValues.length === 0) selectedValues.push(...items.map(datum));
await filter(selectedValues);
updateLegendState();

const { nativeEvent = true } = event;
if (!nativeEvent) return;
if (selectedValues.length === items.length) {
emitter.emit('legend:reset', { nativeEvent });
} else {
// Emit events.
emitter.emit('legend:filter', {
...event,
nativeEvent,
data: {
channel,
values: selectedValues,
},
});
}
};

// Bind and store handlers.
Expand All @@ -124,11 +142,35 @@ function legendFilter(
itemPointerout.set(item, pointerout);
}

const onFilter = async (event) => {
const { nativeEvent } = event;
if (nativeEvent) return;
const { data } = event;
const { channel: specifiedChannel, values } = data;
if (specifiedChannel !== channel) return;
selectedValues = values;
await filter(selectedValues);
updateLegendState();
};

const onEnd = async (event) => {
const { nativeEvent } = event;
if (nativeEvent) return;
selectedValues = items.map(datum);
await filter(selectedValues);
updateLegendState();
};

emitter.on('legend:filter', onFilter);
emitter.on('legend:reset', onEnd);

return () => {
for (const item of items) {
item.removeEventListener('click', itemClick.get(item));
item.removeEventListener('pointerenter', itemPointerenter.get(item));
item.removeEventListener('pointerout', itemPointerout.get(item));
emitter.off('legend:filter', onFilter);
emitter.off('legend:reset', onEnd);
}
};
}
Expand Down Expand Up @@ -168,17 +210,6 @@ export function LegendFilter() {
return update(newOptions);
};

if (!legends.length) {
const onFilter = (options) => {
const { values, channel } = options;
filter(channel, values);
};
emitter.on('legend:filter', onFilter);
return () => {
emitter.off('legend:filter', onFilter);
};
}

const removes = legends.map((legend) => {
const { name: channel, domain } = dataOf(legend).scales[0];
return legendFilter(container, {
Expand All @@ -192,6 +223,8 @@ export function LegendFilter() {
},
filter: (value) => filter(channel, value),
state: legend.attributes.state,
channel,
emitter,
});
});
return () => {
Expand Down

0 comments on commit b7071ef

Please sign in to comment.