diff --git a/__tests__/integration/snapshots/interaction/penguins-point-repeat-matrix-legend-filter/step0.png b/__tests__/integration/snapshots/interaction/penguins-point-repeat-matrix-legend-filter/step0.png new file mode 100644 index 0000000000..af20166a38 Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-repeat-matrix-legend-filter/step0.png differ diff --git a/__tests__/integration/snapshots/static/penguinsPointRepeatMatrix.png b/__tests__/integration/snapshots/static/penguinsPointRepeatMatrix.png new file mode 100644 index 0000000000..51453f57a7 Binary files /dev/null and b/__tests__/integration/snapshots/static/penguinsPointRepeatMatrix.png differ diff --git a/__tests__/plots/interaction/index.ts b/__tests__/plots/interaction/index.ts index a71663e8ff..5a23266c39 100644 --- a/__tests__/plots/interaction/index.ts +++ b/__tests__/plots/interaction/index.ts @@ -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'; diff --git a/__tests__/plots/interaction/penguins-point-repeat-matrix-legend-filter.ts b/__tests__/plots/interaction/penguins-point-repeat-matrix-legend-filter.ts new file mode 100644 index 0000000000..6fc6f5b1b9 --- /dev/null +++ b/__tests__/plots/interaction/penguins-point-repeat-matrix-legend-filter.ts @@ -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')]; +}; diff --git a/__tests__/plots/static/liquid-custom-shape.ts b/__tests__/plots/static/liquid-custom-shape.ts index 9ae06aaef9..050647e8be 100644 --- a/__tests__/plots/static/liquid-custom-shape.ts +++ b/__tests__/plots/static/liquid-custom-shape.ts @@ -30,3 +30,5 @@ export function liquidCustomShape(): G2Spec { }, }; } + +liquidCustomShape.maxError = 10; diff --git a/__tests__/plots/static/penguins-point-repeat-matrix.ts b/__tests__/plots/static/penguins-point-repeat-matrix.ts index 0e257c77fe..501bccc00c 100644 --- a/__tests__/plots/static/penguins-point-repeat-matrix.ts +++ b/__tests__/plots/static/penguins-point-repeat-matrix.ts @@ -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: [ { @@ -29,6 +24,3 @@ export function penguinsPointRepeatMatrix(): G2Spec { ], }; } - -// @todo Remove this, it now has some performance issue. -penguinsPointRepeatMatrix.skip = true; diff --git a/src/composition/repeatMatrix.ts b/src/composition/repeatMatrix.ts index 1d9a165c90..492faccee7 100644 --- a/src/composition/repeatMatrix.ts +++ b/src/composition/repeatMatrix.ts @@ -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'; @@ -63,7 +68,7 @@ const setChildren = useOverrideAdaptor((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 = { @@ -103,6 +108,10 @@ const setChildren = useOverrideAdaptor((options) => { x: fx, y: fy, }), + interaction: deepMix({}, interaction, { + // Register this interaction in parent node. + legendFilter: false, + }), ...rest, }; }); diff --git a/src/interaction/legendFilter.ts b/src/interaction/legendFilter.ts index 5f591ad65a..742c892c87 100644 --- a/src/interaction/legendFilter.ts +++ b/src/interaction/legendFilter.ts @@ -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); @@ -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, @@ -269,7 +294,11 @@ 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, @@ -277,7 +306,11 @@ export function LegendFilter() { } 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, });