Skip to content

Commit

Permalink
fix(interaction): muti iteraction (#5419)
Browse files Browse the repository at this point in the history
  • Loading branch information
pearmini authored Aug 17, 2023
1 parent db45440 commit 328aeea
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 126 deletions.
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.
50 changes: 50 additions & 0 deletions __tests__/plots/interaction/countries-bubble-multi-legends.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { G2Spec } from '../../../src';
import { LEGEND_ITEMS_CLASS_NAME } from '../../../src/interaction/legendFilter';
import { SLIDER_CLASS_NAME } from '../../../src/interaction/sliderFilter';
import { dispatchValueChange } from './appl-line-slider-filter';
import { step } from './utils';

export function countriesBubbleMultiLegends(): G2Spec {
return {
type: 'point',
padding: 'auto',
data: {
type: 'fetch',
value: 'data/countries.json',
},
encode: {
x: 'change in female rate',
y: 'change in male rate',
size: 'pop',
color: 'continent',
shape: 'point',
},
scale: {
color: { range: ['#ffd500', '#82cab2', '#193442', '#d18768', '#7e827a'] },
x: { nice: true },
y: { nice: true },
size: { range: [4, 30] },
},
style: { stroke: '#bbb', fillOpacity: 0.8 },
slider: {
x: { labelFormatter: (d) => d.toFixed(1) },
y: { labelFormatter: (d) => d.toFixed(1) },
},
};
}

countriesBubbleMultiLegends.steps = ({ canvas }) => {
const { document } = canvas;
const elements = document.getElementsByClassName(LEGEND_ITEMS_CLASS_NAME);
const [e0] = elements;
const sliders = document.getElementsByClassName(SLIDER_CLASS_NAME);
const [s1] = sliders;
return [
step(e0, 'click'),
{
changeState: () => {
dispatchValueChange(s1);
},
},
];
};
1 change: 1 addition & 0 deletions __tests__/plots/interaction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,4 @@ export { unemploymentAreaLegendFilterPages } from './unemployment-area-legend-fi
export { mockAreaSliderFilterLabel } from './mock-area-slider-filter-label';
export { commitsPointLegendFilter } from './commits-point-legend-filter';
export { settleWeatherLegendFilter } from './seattle-weather-legend-filter';
export { countriesBubbleMultiLegends } from './countries-bubble-multi-legends';
59 changes: 31 additions & 28 deletions src/interaction/brushFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function brushFilter(

export function BrushFilter({ hideX = true, hideY = true, ...rest }) {
return (target, viewInstances, emitter) => {
const { container, view, options: viewOptions, update } = target;
const { container, view, options: viewOptions, update, setState } = target;
const plotArea = selectPlotArea(container);
const defaultOptions = {
maskFill: '#777',
Expand Down Expand Up @@ -110,40 +110,43 @@ export function BrushFilter({ hideX = true, hideY = true, ...rest }) {

// Update the domain of x and y scale to filter data.
const [domainX, domainY] = selection;
const { marks } = viewOptions;
const newMarks = marks.map((mark) =>
deepMix(
{
// Hide label to keep smooth transition.
axis: {
...(hideX && { x: { transform: [{ type: 'hide' }] } }),
...(hideY && { y: { transform: [{ type: 'hide' }] } }),

setState('brushFilter', (options) => {
const { marks } = options;
const newMarks = marks.map((mark) =>
deepMix(
{
// Hide label to keep smooth transition.
axis: {
...(hideX && { x: { transform: [{ type: 'hide' }] } }),
...(hideY && { y: { transform: [{ type: 'hide' }] } }),
},
},
},
mark,
{
// Set nice to false to avoid modify domain.
scale: {
x: { domain: domainX, nice: false },
y: { domain: domainY, nice: false },
mark,
{
// Set nice to false to avoid modify domain.
scale: {
x: { domain: domainX, nice: false },
y: { domain: domainY, nice: false },
},
},
},
),
);
),
);

return {
...viewOptions,
marks: newMarks,
clip: true, // Clip shapes out of plot area.
};
});

// Emit event.
emitter.emit('brush:filter', {
...event,
data: { selection: [domainX, domainY] },
});

// Rerender and update view.
const newOptions = {
...viewOptions,
marks: newMarks,
clip: true, // Clip shapes out of plot area.
};
const newState = await update(newOptions);
const newState = await update();
newView = newState.view;
filtering = false;
filtered = true;
Expand All @@ -160,10 +163,10 @@ export function BrushFilter({ hideX = true, hideY = true, ...rest }) {
...event,
data: { selection: [domainX, domainY] },
});

filtered = false;
newView = view;
update(viewOptions);
setState('brushFilter');
update();
},
extent: undefined,
emitter,
Expand Down
49 changes: 26 additions & 23 deletions src/interaction/chartIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function ChartIndex({
...style
}: Record<string, any>) {
return (context) => {
const { options, view, container, update } = context;
const { view, container, update, setState } = context;
const { markState, scale, coordinate } = view;

// Get line mark value, exit if it is not existed.
Expand All @@ -70,18 +70,6 @@ export function ChartIndex({
const I = Y.map((_, i) => i);
const sortedX: number[] = sort(I.map((i) => X[i]));

// Clone options and get line mark.
const clonedOptions = deepMix({}, options);
const lineMark = clonedOptions.marks.find((d) => d.type === 'line');

// Update domain of y scale for the line mark.
const r = (I: number[]) => max(I, (i) => +Y[i]) / min(I, (i) => +Y[i]);
const k = max(rollup(I, r, (i) => S[i]).values());
const domainY = [1 / k, k];
deepMix(lineMark, {
scale: { y: { domain: domainY } },
});

// Prepare shapes.
const plotArea = selectPlotArea(container);
const lines = container.getElementsByClassName(ELEMENT_CLASS_NAME);
Expand Down Expand Up @@ -142,16 +130,31 @@ export function ChartIndex({

updateRule(focus, date);

// Update normalize options.
const normalizeY = maybeTransform(lineMark);
normalizeY.groupBy = 'color';
normalizeY.basis = (I, Y) => {
const i = I[bisector((i) => X[+i]).center(I, date)];
return Y[i];
};
// Disable animation.
for (const mark of clonedOptions.marks) mark.animate = false;
const newState = await update(clonedOptions);
setState('chartIndex', (options) => {
// Clone options and get line mark.
const clonedOptions = deepMix({}, options);
const lineMark = clonedOptions.marks.find((d) => d.type === 'line');

// Update domain of y scale for the line mark.
const r = (I: number[]) => max(I, (i) => +Y[i]) / min(I, (i) => +Y[i]);
const k = max(rollup(I, r, (i) => S[i]).values());
const domainY = [1 / k, k];
deepMix(lineMark, {
scale: { y: { domain: domainY } },
});
// Update normalize options.
const normalizeY = maybeTransform(lineMark);
normalizeY.groupBy = 'color';
normalizeY.basis = (I, Y) => {
const i = I[bisector((i) => X[+i]).center(I, date)];
return Y[i];
};
// Disable animation.
for (const mark of clonedOptions.marks) mark.animate = false;
return clonedOptions;
});

const newState = await update();
newView = newState.view;
};

Expand Down
28 changes: 16 additions & 12 deletions src/interaction/fisheye.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,30 @@ export function Fisheye({
trailing = false,
}: Record<string, any>) {
return (context) => {
const { options, update, container } = context;
const { options, update, setState, container } = context;
const plotArea = selectPlotArea(container);

// Clone options and mutate it.
// Disable animation.
const clonedOptions = deepMix({}, options);
for (const mark of clonedOptions.marks) mark.animate = false;
const updateFocus = throttle(
(event) => {
const focus = mousePosition(plotArea, event);
if (!focus) {
update(options);
setState('fisheye');
update();
return;
}
const [x, y] = focus;
const fisheye = maybeCoordinate(clonedOptions);
fisheye.focusX = x;
fisheye.focusY = y;
fisheye.visual = true;
update(clonedOptions);
setState('fisheye', (options) => {
// Clone options and mutate it.
// Disable animation.
const clonedOptions = deepMix({}, options);
for (const mark of clonedOptions.marks) mark.animate = false;
const [x, y] = focus;
const fisheye = maybeCoordinate(clonedOptions);
fisheye.focusX = x;
fisheye.focusY = y;
fisheye.visual = true;
return clonedOptions;
});
update();
},
wait,
{ leading, trailing },
Expand Down
85 changes: 45 additions & 40 deletions src/interaction/legendFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,60 +200,65 @@ function legendFilterContinuous(_, { legend, filter, emitter, channel }) {

export function LegendFilter() {
return (context, _, emitter) => {
const { container, view, options: viewOptions, update } = context;
const { container, view, update, setState } = context;

const channelsOf = (legend) => {
return dataOf(legend).scales.map((d) => d.name);
};
const legends = [
...legendsOf(container),
...legendsContinuousOf(container),
];
const allChannels = legends.flatMap(channelsOf);

const filter = throttle(
async (channel, value, ordinal: boolean, channels) => {
const { marks } = viewOptions;
// Add filter transform for every marks,
// which will skip for mark without color channel.
const newMarks = marks.map((mark) => {
if (mark.type === 'legends') return mark;

// Inset after aggregate transform, such as group, and bin.
const { transform = [] } = mark;
const index = transform.findIndex(
({ type }) => type.startsWith('group') || type.startsWith('bin'),
);
const newTransform = [...transform];
newTransform.splice(index + 1, 0, {
type: 'filter',
[channel]: { value, ordinal },
});

// Set domain of scale to preserve encoding.
const newScale = Object.fromEntries(
channels.map((channel) => [
channel,
{ domain: view.scale[channel].getOptions().domain },
]),
);

return deepMix({}, mark, {
transform: newTransform,
scale: newScale,
...(!ordinal && { animate: false }),
legend: { [channel]: { preserve: true } },
async (legend, channel, value, ordinal: boolean, channels) => {
setState(legend, (viewOptions) => {
const { marks } = viewOptions;
// Add filter transform for every marks,
// which will skip for mark without color channel.
const newMarks = marks.map((mark) => {
if (mark.type === 'legends') return mark;

// Inset after aggregate transform, such as group, and bin.
const { transform = [] } = mark;
const index = transform.findIndex(
({ type }) => type.startsWith('group') || type.startsWith('bin'),
);
const newTransform = [...transform];
newTransform.splice(index + 1, 0, {
type: 'filter',
[channel]: { value, ordinal },
});

// Set domain of scale to preserve encoding.
const newScale = Object.fromEntries(
channels.map((channel) => [
channel,
{ domain: view.scale[channel].getOptions().domain },
]),
);

return deepMix({}, mark, {
transform: newTransform,
scale: newScale,
...(!ordinal && { animate: false }),
legend: Object.fromEntries(
allChannels.map((d) => [d, { preserve: true }]),
),
});
});
return { ...viewOptions, marks: newMarks };
});
const newOptions = {
...viewOptions,
marks: newMarks,
};
await update(newOptions);
await update();
},
50,
{ trailing: true },
);

const removes = legends.map((legend) => {
const { name: channel, domain } = dataOf(legend).scales[0];
const channels = dataOf(legend).scales.map((d) => d.name);
const channels = channelsOf(legend);
if (legend.className === CATEGORY_LEGEND_CLASS_NAME) {
return legendFilterOrdinal(container, {
legends: itemsOf,
Expand All @@ -264,15 +269,15 @@ export function LegendFilter() {
const { index } = datum;
return domain[index];
},
filter: (value) => filter(channel, value, true, channels),
filter: (value) => filter(legend, channel, value, true, channels),
state: legend.attributes.state,
channel,
emitter,
});
} else {
return legendFilterContinuous(container, {
legend,
filter: (value) => filter(channel, value, false, channels),
filter: (value) => filter(legend, channel, value, false, channels),
emitter,
channel,
});
Expand Down
Loading

0 comments on commit 328aeea

Please sign in to comment.