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

fix(interaction): share states #5419

Merged
merged 1 commit into from
Aug 17, 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
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