Skip to content

Commit

Permalink
feat(interaction): emit slider filter (#5114)
Browse files Browse the repository at this point in the history
  • Loading branch information
pearmini committed May 29, 2023
1 parent 9a76ca9 commit 32ac0f6
Show file tree
Hide file tree
Showing 12 changed files with 289 additions and 23 deletions.
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';

Check warning on line 6 in __tests__/integration/api-chart-emit-slider-filter.spec.ts

View workflow job for this annotation

GitHub Actions / build

`../../src/interaction/sliderFilter` import should occur before import of `./utils/createNodeGCanvas`
import { dispatchValueChange } from '../plots/tooltip/appl-line-slider-filter';

Check warning on line 7 in __tests__/integration/api-chart-emit-slider-filter.spec.ts

View workflow job for this annotation

GitHub Actions / build

`../plots/tooltip/appl-line-slider-filter` import should occur before import of `./utils/createNodeGCanvas`
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();
});
});
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.
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);
}
};
};
}

0 comments on commit 32ac0f6

Please sign in to comment.