Skip to content

Commit

Permalink
feat(facet): support legend filter (#5593)
Browse files Browse the repository at this point in the history
  • Loading branch information
pearmini authored Sep 25, 2023
1 parent b39be0a commit 05a49ac
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 63 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.
1 change: 1 addition & 0 deletions __tests__/plots/interaction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,4 @@ export { countriesBubbleMultiLegends } from './countries-bubble-multi-legends';
export { pointsPointTooltipMarker } from './points-point-tooltip-marker';
export { mockPieInteraction } from './mock-pie-interaction';
export { missingAreaTooltipMarker } from './missing-area-tooltip-marker';
export { penguinsPointRepeatMatrixLegendFilter } from './penguins-point-repeat-matrix-legend-filter';
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { G2Spec } from '../../../src';
import { LEGEND_ITEMS_CLASS_NAME } from '../../../src/interaction/legendFilter';
import { step } from './utils';

export function penguinsPointRepeatMatrixLegendFilter(): G2Spec {
return {
type: 'repeatMatrix',
width: 480,
height: 480,
paddingLeft: 50,
paddingBottom: 50,
data: {
type: 'fetch',
value: 'data/penguins.csv',
},
encode: {
position: ['culmen_length_mm', 'culmen_depth_mm'],
},
children: [
{
type: 'point',
encode: {
color: 'species',
},
},
],
};
}

penguinsPointRepeatMatrixLegendFilter.steps = ({ canvas }) => {
const { document } = canvas;
const elements = document.getElementsByClassName(LEGEND_ITEMS_CLASS_NAME);
const [e0] = elements;
return [step(e0, 'click')];
};
2 changes: 2 additions & 0 deletions __tests__/plots/static/liquid-custom-shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ export function liquidCustomShape(): G2Spec {
},
};
}

liquidCustomShape.maxError = 10;
18 changes: 5 additions & 13 deletions __tests__/plots/static/penguins-point-repeat-matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,16 @@ import { G2Spec } from '../../../src';
export function penguinsPointRepeatMatrix(): G2Spec {
return {
type: 'repeatMatrix',
width: 800,
height: 800,
paddingLeft: 70,
paddingBottom: 70,
width: 480,
height: 480,
paddingLeft: 50,
paddingBottom: 50,
data: {
type: 'fetch',
value: 'data/penguins.csv',
},
encode: {
position: [
'culmen_length_mm',
'culmen_depth_mm',
'flipper_length_mm',
'body_mass_g',
],
position: ['culmen_length_mm', 'culmen_depth_mm'],
},
children: [
{
Expand All @@ -29,6 +24,3 @@ export function penguinsPointRepeatMatrix(): G2Spec {
],
};
}

// @todo Remove this, it now has some performance issue.
penguinsPointRepeatMatrix.skip = true;
13 changes: 11 additions & 2 deletions src/composition/repeatMatrix.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { deepMix } from '@antv/util';
import { CompositionComponent as CC, G2ViewTree, Node } from '../runtime';
import {
CompositionComponent as CC,
G2View,
G2ViewTree,
Node,
} from '../runtime';
import { RepeatMatrixComposition } from '../spec';
import { Container } from '../utils/container';
import { calcBBox } from '../utils/vector';
Expand Down Expand Up @@ -63,7 +68,7 @@ const setChildren = useOverrideAdaptor<G2ViewTree>((options) => {
const facet = facets[i];
const children = normalizedChildren[i];
return children.map((d) => {
const { scale, key, encode, axis, ...rest } = d;
const { scale, key, encode, axis, interaction, ...rest } = d;
const guideY = scale?.y?.guide;
const guideX = scale?.x?.guide;
const defaultScale = {
Expand Down Expand Up @@ -103,6 +108,10 @@ const setChildren = useOverrideAdaptor<G2ViewTree>((options) => {
x: fx,
y: fy,
}),
interaction: deepMix({}, interaction, {
// Register this interaction in parent node.
legendFilter: false,
}),
...rest,
};
});
Expand Down
129 changes: 81 additions & 48 deletions src/interaction/legendFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,69 @@ function legendFilterContinuous(_, { legend, filter, emitter, channel }) {
};
}

async function filterView(
context, // View instance,
{
legend, // Legend instance.
channel, // Filter Channel.
value, // Filtered Values.
ordinal, // Data type of the legend.
channels, // Channels for this legend.
allChannels, // Channels for all legends.
facet = false, // For facet.
},
) {
const { view, update, setState } = context;
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: facet
? false
: Object.fromEntries(allChannels.map((d) => [d, { preserve: true }])),
});
});
return { ...viewOptions, marks: newMarks };
});
await update();
}

function filterFacets(facets, options) {
for (const facet of facets) {
filterView(facet, { ...options, facet: true });
}
}

export function LegendFilter() {
return (context, _, emitter) => {
const { container, view, update, setState } = context;
return (context, contexts, emitter) => {
const { container } = context;
const facets = contexts.filter((d) => d !== context);
const isFacet = facets.length > 0;

const channelsOf = (legend) => {
return dataOf(legend).scales.map((d) => d.name);
Expand All @@ -211,54 +271,19 @@ export function LegendFilter() {
];
const allChannels = legends.flatMap(channelsOf);

const filter = throttle(
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 };
});
await update();
},
50,
{ trailing: true },
);
const filter = isFacet
? throttle(filterFacets, 50, { trailing: true })
: throttle(filterView, 50, { trailing: true });

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

0 comments on commit 05a49ac

Please sign in to comment.