Skip to content

Commit

Permalink
feat(facet): support legend filter
Browse files Browse the repository at this point in the history
  • Loading branch information
pearmini committed Sep 25, 2023
1 parent b39be0a commit 413526b
Show file tree
Hide file tree
Showing 7 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.
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 413526b

Please sign in to comment.