Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(interaction): emit slider filter #5114

Merged
merged 1 commit into from
May 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion __tests__/integration/api-chart-emit-legend-filter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { sleep } from './utils/sleep';
import { kebabCase } from './utils/kebabCase';
import './utils/useSnapshotMatchers';

describe('chart.options.autoFit', () => {
describe('chart.emit', () => {
const dir = `${__dirname}/snapshots/api/${kebabCase(render.name)}`;
const canvas = createNodeGCanvas(800, 500);
let chart;
Expand Down
76 changes: 76 additions & 0 deletions __tests__/integration/api-chart-emit-slider-filter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { chartEmitSliderFilter as render } from '../plots/api/chart-emit-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';

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

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

const [SX, SY] = Array.from(
canvas.document.getElementsByClassName(SLIDER_CLASS_NAME),
);

// chart.emit('sliderX:filter', options) should trigger slider.
const X = ['2001-01', '2001-03'];
chart.emit('sliderX:filter', {
data: { selection: [X, undefined] },
});
await sleep(100);
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 expect(canvas).toMatchCanvasSnapshot(dir, 'step1');

chart.off();

// chart.on("sliderX:filter") should receive expected data.
const [filteredX, resolveX] = createPromise();
chart.on('sliderX:filter', (event) => {
if (!event.nativeEvent) return;
expect(event.data.selection).toEqual([
['2001-05', '2002-03'],
[50, 550],
]);
resolveX();
});
dispatchValueChange(SX);
await sleep(20);
await filteredX;

// chart.on("sliderY:filter") should receive expected data.
const [filteredY, resolveY] = createPromise();
chart.on('sliderY:filter', (event) => {
if (!event.nativeEvent) return;
expect(event.data.selection).toEqual([
['2001-05', '2002-03'],
[150, 450],
]);
resolveY();
});
dispatchValueChange(SY);
await sleep(20);
await filteredY;
});

afterAll(() => {
canvas?.destroy();
});
});
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.
91 changes: 91 additions & 0 deletions __tests__/plots/api/chart-emit-slider-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Chart } from '../../../src';

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

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

const buttonY = document.createElement('button');
buttonY.innerText = 'FilterY';
container.appendChild(buttonY);

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

const chart = new Chart({
theme: 'classic',
container: wrapperDiv,
paddingLeft: 80,
paddingBottom: 120,
canvas,
});

chart.options({
type: 'interval',
data: [
{ date: '2001-01', value: 100 },
{ date: '2001-02', value: 400 },
{ date: '2001-03', value: 500 },
{ date: '2001-04', value: 600 },
{ date: '2001-05', value: 300 },
{ date: '2001-06', value: 600 },
{ date: '2001-07', value: 300 },
{ date: '2001-08', value: 600 },
{ date: '2001-09', value: 109 },
{ date: '2001-10', value: 100 },
{ date: '2001-11', value: 102 },
{ date: '2001-12', value: 103 },
{ date: '2002-01', value: 102 },
{ date: '2002-02', value: 101 },
{ date: '2002-03', value: 200 },
{ date: '2002-04', value: 500 },
{ date: '2002-05', value: 100 },
{ date: '2002-06', value: 100 },
{ date: '2002-07', value: 102 },
{ date: '2002-08', value: 109 },
],
encode: { x: 'date', y: 'value' },
axis: { x: { size: 100, style: { labelTransform: 'rotate(90)' } } },
slider: { x: {}, y: { labelFormatter: (d) => +d.toFixed(1) + '' } },
});

const finished = chart.render();

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

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

let resolveX;
const filterX = new Promise((r) => (resolveX = r));

buttonX.onclick = () => {
const X = ['2001-01', '2001-03'];
chart.emit('sliderX:filter', {
data: { selection: [X, undefined] },
});
resolveX();
};

let resolveY;
const filterY = new Promise((r) => (resolveY = r));

buttonY.onclick = () => {
const Y = [50, 550];
chart.emit('sliderY:filter', {
data: { selection: [undefined, Y] },
});
resolveY();
};

return { chart, buttonX, buttonY, finished, filterX, filterY };
}
1 change: 1 addition & 0 deletions __tests__/plots/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export { chartRenderUpdateNonAnimation } from './chart-render-update-non-animati
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';
1 change: 1 addition & 0 deletions __tests__/plots/static/cars3-line-parallel-horizontal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function cars3LineParallelHorizontal(): G2Spec {
type: 'fetch',
value: 'data/cars3.csv',
},
interaction: { tooltip: { series: false } },
coordinate: { type: 'parallel' },
encode: {
position,
Expand Down
1 change: 1 addition & 0 deletions __tests__/plots/static/cars3-line-parallel-vertical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function cars3LineParallelVertical(): G2Spec {
type: 'fetch',
value: 'data/cars3.csv',
},
interaction: { tooltip: { series: false } },
coordinate: { type: 'parallel', transform: [{ type: 'transpose' }] },
encode: {
position,
Expand Down
1 change: 1 addition & 0 deletions __tests__/plots/tooltip/appl-line-slider-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function dispatchValueChange(slider, values = [0.25, 0.75]) {
slider.update({ values });
slider.dispatchEvent(
new CustomEvent('valuechange', {
nativeEvent: true,
detail: {
value: [0.25, 0.75],
},
Expand Down
28 changes: 28 additions & 0 deletions site/docs/spec/interaction/sliderFilter.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,31 @@ chart

chart.render();
```

## 案例

### 触发事件

```js
chart.emit('sliderX:filter', {
data: { selection: [['2001-01', '2001-03'], undefined] },
});

chart.emit('sliderY:filter', {
data: { selection: [undefined, [50, 550]] },
});
```

### 监听数据

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

chart.on('sliderY:filter', (event) => {
const { data, nativeEvent } = event;
if (nativeEvent) console.log('sliderY:filter', data.selection);
});
```
92 changes: 77 additions & 15 deletions src/interaction/sliderFilter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { deepMix, throttle } from '@antv/util';
import { deepMix, throttle, upperFirst } from '@antv/util';
import { CustomEvent } from '@antv/g';
import { isTranspose } from '../utils/coordinate';
import { invert, domainOf } from '../utils/scale';
import { invert, domainOf, abstractOf } from '../utils/scale';

export const SLIDER_CLASS_NAME = 'slider';

Expand Down Expand Up @@ -45,6 +46,10 @@ function abstractValue(values, scale, reverse) {
return domainOf(scale, [d0, d1]);
}

function extentOf(domain) {
return [domain[0], domain[domain.length - 1]];
}

/**
* @todo Support click to reset after fix click and dragend conflict.
*/
Expand All @@ -53,7 +58,7 @@ export function SliderFilter({
leading = true,
trailing = false,
}: any) {
return (context) => {
return (context, _, emitter) => {
const { container, view, options, update } = context;
const sliders = container.getElementsByClassName(SLIDER_CLASS_NAME);
if (!sliders.length) return () => {};
Expand All @@ -71,6 +76,7 @@ export function SliderFilter({
};

const sliderHandler = new Map();
const emitHandlers = new Set<[string, (event: any) => void]>();

// Store current domain of x and y scale.
const channelDomain = {
Expand All @@ -80,47 +86,103 @@ export function SliderFilter({

for (const slider of sliders) {
const { orientation } = slider.attributes;
const [channel0, channel1] = channelOf(orientation);
const eventName = `slider${upperFirst(channel0)}:filter`;
const isX = channel0 === 'x';
const domainsOf = (event) => {
// From abstract values.
if (event.data) {
const { selection } = event.data;
const [X = extentOf(channelDomain.x), Y = extentOf(channelDomain.y)] =
selection;
return isX
? [domainOf(scaleX, X), domainOf(scaleY, Y)]
: [domainOf(scaleY, Y), domainOf(scaleX, X)];
}

// From visual values.
const { value: values } = event.detail;
const scale0 = scale[channel0];
const domain0 = abstractValue(
values,
scale0,
transposed && orientation === 'horizontal',
);
const domain1 = channelDomain[channel1];
return [domain0, domain1];
};

const onValueChange = throttle(
async (event) => {
const { value: values } = event.detail;
if (filtering) return;
filtering = true;

const [channel0, channel1] = channelOf(orientation);
const { nativeEvent = true } = event;

// Update domain of the current channel.
const scale0 = scale[channel0];
const domain0 = abstractValue(
values,
scale0,
transposed && orientation === 'horizontal',
);
// Get and update domain.
const [domain0, domain1] = domainsOf(event);
channelDomain[channel0] = domain0;

// Get domain of the other channel.
const domain1 = channelDomain[channel1];
channelDomain[channel1] = domain1;

// Filter data.
const newOptions = filterDataByDomain(options, {
[channel0]: { domain: domain0 },
[channel1]: { domain: domain1 },
});

if (nativeEvent) {
// Emit events.
const X = isX ? domain0 : domain1;
const Y = isX ? domain1 : domain0;
emitter.emit(eventName, {
...event,
nativeEvent,
data: { selection: [extentOf(X), extentOf(Y)] },
});
}

await update(newOptions);
filtering = false;
},
wait,
{ leading, trailing },
);

const emitHandler = (event) => {
const { nativeEvent } = event;
if (nativeEvent) return;

const { data } = event;
const { selection } = data;
const [X, Y] = selection;

// Update data.
slider.dispatchEvent(
new CustomEvent('valuechange', {
data,
nativeEvent: false,
}),
);

// Update slider.
const V = isX ? abstractOf(X, scaleX) : abstractOf(Y, scaleY);
slider.setValues(V);
};

emitter.on(eventName, emitHandler);

slider.addEventListener('valuechange', onValueChange);
sliderHandler.set(slider, onValueChange);
emitHandlers.add([eventName, emitHandler]);
}

return () => {
for (const [slider, handler] of sliderHandler) {
slider.removeEventListener('valuechange', handler);
}
for (const [name, handler] of emitHandlers) {
emitter.off(name, handler);
}
};
};
}
Loading