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

feat(legend): continuous filter #5356

Merged
merged 1 commit into from
Aug 2, 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.
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.
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.
Binary file modified __tests__/integration/snapshots/static/aaplLineMissingDataTrial.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __tests__/integration/snapshots/static/barleyLineTrail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __tests__/integration/snapshots/static/bodyPointScatterPlot.png
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.
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.
Binary file modified __tests__/integration/snapshots/static/cars3LineRadar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __tests__/integration/snapshots/static/commitsPointGrouped.png
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.
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.
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.
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.
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.
Binary file modified __tests__/integration/snapshots/static/disastersPointBubble.png
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.
Binary file modified __tests__/integration/snapshots/static/haleChoroplethWorld.png
Binary file modified __tests__/integration/snapshots/static/moviesPointBin.png
Binary file modified __tests__/integration/snapshots/static/moviesRectBin.png
Binary file modified __tests__/integration/snapshots/static/moviesRectBinOpacity.png
Binary file modified __tests__/integration/snapshots/static/settleWeatherCellLineXY.png
Binary file modified __tests__/integration/snapshots/static/stocksLineSeriesGradient.png
Binary file modified __tests__/integration/snapshots/static/unemploymentChoropleth.png
54 changes: 54 additions & 0 deletions __tests__/plots/interaction/commits-point-legend-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { G2Spec } from '../../../src';
import { CONTINUOUS_LEGEND_CLASS_NAME } from '../../../src/interaction/legendFilter';

export function commitsPointLegendFilter(): G2Spec {
return {
type: 'point',
height: 300,
inset: 10,
padding: 'auto',
frame: true,
data: {
type: 'fetch',
value: 'data/commits.csv',
},
encode: {
x: (d) => d.time.getUTCHours(),
y: (d) => d.time.getUTCDay(),
size: 'count',
shape: 'point',
color: 'count',
},
transform: [
{ type: 'group', size: 'sum', color: 'sum' },
{ type: 'sortY' },
],
scale: {
y: { type: 'point' },
x: { tickCount: 24 },
color: { palette: 'rdBu' },
},
axis: {
x: { title: 'time (hours)' },
y: { title: 'time (day)', grid: true },
},
};
}

export function dispatchValueChange(legend, values) {
legend.setSelection(...values);
}

commitsPointLegendFilter.steps = ({ canvas }) => {
const { document } = canvas;
const [legend] = document.getElementsByClassName(
CONTINUOUS_LEGEND_CLASS_NAME,
);
return [
{
changeState: () => {
dispatchValueChange(legend, [20, 60]);
},
},
];
};
2 changes: 2 additions & 0 deletions __tests__/plots/interaction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,5 @@ export { weatherLineLegendMark } from './weather-line-legend-mark';
export { countriesAnnotationSliderFilter } from './countries-annotation-slider-filter';
export { unemploymentAreaLegendFilterPages } from './unemployment-area-legend-filter-pages';
export { mockAreaSliderFilterLabel } from './mock-area-slider-filter-label';
export { commitsPointLegendFilter } from './commits-point-legend-filter';
export { settleWeatherLegendFilter } from './seattle-weather-legend-filter';
45 changes: 45 additions & 0 deletions __tests__/plots/interaction/seattle-weather-legend-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { G2Spec } from '../../../src';
import { CONTINUOUS_LEGEND_CLASS_NAME } from '../../../src/interaction/legendFilter';
import { dispatchValueChange } from './commits-point-legend-filter';

export function settleWeatherLegendFilter(): G2Spec {
return {
type: 'cell',
height: 330,
data: {
type: 'fetch',
value: 'data/seattle-weather.csv',
},
encode: {
x: (d) => new Date(d.date).getUTCDate(),
y: (d) => new Date(d.date).getUTCMonth(),
color: 'temp_max',
},
transform: [{ type: 'group', color: 'max' }],
scale: {
x: { compare: (a, b) => +a - +b },
y: { compare: (a, b) => +a - +b },
},
legend: { color: { layout: { justifyContent: 'flex-start' } } },
style: { inset: 0.5 },
};
}

settleWeatherLegendFilter.steps = ({ canvas }) => {
const { document } = canvas;
const [legend] = document.getElementsByClassName(
CONTINUOUS_LEGEND_CLASS_NAME,
);
return [
{
changeState: () => {
dispatchValueChange(legend, [10, 30]);
},
},
{
changeState: () => {
dispatchValueChange(legend, [5, 36.4]);
},
},
];
};
6 changes: 0 additions & 6 deletions __tests__/plots/static/commits-point-grouped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,5 @@ export function commitsPointGrouped(): G2Spec {
x: { title: 'time (hours)' },
y: { title: 'time (day)', grid: true },
},
// viewStyle: {
// viewFill: '#4e79a7',
// plotFill: '#f28e2c',
// mainFill: '#e15759',
// contentFill: '#76b7b2',
// },
};
}
19 changes: 5 additions & 14 deletions __tests__/plots/static/seattle-weather-cell-grouped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,17 @@ export function settleWeatherCellGrouped(): G2Spec {
type: 'fetch',
value: 'data/seattle-weather.csv',
},
transform: [{ type: 'group', color: 'max' }],
encode: {
x: (d) => new Date(d.date).getUTCDate(),
y: (d) => new Date(d.date).getUTCMonth(),
color: 'temp_max',
},
legend: {
color: {
layout: {
justifyContent: 'flex-start',
},
},
},
style: {
inset: 0.5,
},
transform: [{ type: 'group', color: 'max' }],
scale: {
color: {
palette: 'gnBu',
},
x: { compare: (a, b) => +a - +b },
y: { compare: (a, b) => +a - +b },
},
legend: { color: { layout: { justifyContent: 'flex-start' } } },
style: { inset: 0.5 },
};
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"@antv/g": "^5.18.6",
"@antv/g-canvas": "^1.11.5",
"@antv/g-plugin-dragndrop": "^1.8.3",
"@antv/gui": "0.5.0-alpha.26",
"@antv/gui": "0.5.0-alpha.28",
"@antv/path-util": "^3.0.1",
"@antv/scale": "^0.4.7",
"@antv/util": "^3.3.2",
Expand Down
14 changes: 9 additions & 5 deletions src/component/legendContinuous.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DisplayObject, parseColor } from '@antv/g';
import { Continuous } from '@antv/gui';
import { Quantile, Quantize, Threshold } from '@antv/scale';
import { Constant, Quantile, Quantize, Threshold } from '@antv/scale';
import { format } from 'd3-format';
import type {
FlexLayout,
Expand Down Expand Up @@ -43,12 +43,9 @@ type Config = {
color: string[];
data: any[];
labelFilter?: (datum: any, index: number) => boolean;
domain?: [number, number];
};

function calculateFinalSize(size: number, defaultSize: number): number {
return Math.min(size, defaultSize);
}

function updateShapeDimensions(
shape: Shape,
finalSize: number,
Expand Down Expand Up @@ -154,8 +151,14 @@ function getLinearConfig(

const scale = colorScale || createColorScale(definedScale, defaultColor);
const [min, max] = rangeOf(scale);
const [domainMin, domainMax] = rangeOf(
[colorScale, sizeScale, opacityScale]
.filter((d) => d !== undefined)
.find((d) => !(d instanceof Constant)),
);
return {
...shape,
domain: [domainMin, domainMax],
data: scale.getTicks().map((value) => ({ value })),
color: new Array(length).fill(0).map((d, i) => {
const value = ((max - min) / (length - 1)) * i + min;
Expand Down Expand Up @@ -269,6 +272,7 @@ export const LegendContinuous: GCC<LegendContinuousOptions> = (options) => {

layoutWrapper.appendChild(
new Continuous({
className: 'legend-continuous',
style: finalStyle,
}),
);
Expand Down
145 changes: 96 additions & 49 deletions src/interaction/legendFilter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { DisplayObject } from '@antv/g';
import { deepMix } from '@antv/util';
import { deepMix, throttle } from '@antv/util';
import { subObject } from '../utils/helper';
import { useState, setCursor, restoreCursor } from './utils';

export const CATEGORY_LEGEND_CLASS_NAME = 'legend-category';

export const CONTINUOUS_LEGEND_CLASS_NAME = 'legend-continuous';

export const LEGEND_ITEMS_CLASS_NAME = 'items-item';

export const LEGEND_MAKER_CLASS_NAME = 'legend-category-item-marker';
Expand All @@ -27,6 +29,10 @@ export function legendsOf(root) {
return root.getElementsByClassName(CATEGORY_LEGEND_CLASS_NAME);
}

export function legendsContinuousOf(root) {
return root.getElementsByClassName(CONTINUOUS_LEGEND_CLASS_NAME);
}

export function dataOf(root) {
// legend -> layout -> container
let parent = root.parentNode;
Expand All @@ -44,7 +50,7 @@ export function attributesOf(root) {
return child.attributes;
}

function legendFilter(
function legendFilterOrdinal(
root: DisplayObject,
{
legends, // given the root of chart returns legends to be manipulated
Expand Down Expand Up @@ -175,61 +181,102 @@ function legendFilter(
};
}

function legendFilterContinuous(_, { legend, filter, emitter, channel }) {
const onValueChange = ({ detail: { value } }) => {
filter(value);
emitter.emit({
nativeEvent: true,
data: {
channel,
values: value,
},
});
};
legend.addEventListener('valuechange', onValueChange);
return () => {
legend.removeEventListener('valuechange', onValueChange);
};
}

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

const legends = legendsOf(container);

const filter = async (channel, value) => {
const { scale } = view;
const { [channel]: scaleOrdinal } = scale;
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;
const { transform = [] } = mark;
const newTransform = [
{ type: 'filter', [channel]: value },
...transform,
];
return deepMix({}, mark, {
transform: newTransform,
//Set domain of scale to preserve legends.
scale: {
[channel]: {
domain: scaleOrdinal.getOptions().domain,
},
},
legend: {
[channel]: { preserve: true },
},
const legends = [
...legendsOf(container),
...legendsContinuousOf(container),
];

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 } },
});
});
});
const newOptions = {
...viewOptions,
marks: newMarks,
};
return update(newOptions);
};
const newOptions = {
...viewOptions,
marks: newMarks,
};
await update(newOptions);
},
50,
{ trailing: true },
);

const removes = legends.map((legend) => {
const { name: channel, domain } = dataOf(legend).scales[0];
return legendFilter(container, {
legends: itemsOf,
marker: markerOf,
label: labelOf,
datum: (d) => {
const { __data__: datum } = d;
const { index } = datum;
return domain[index];
},
filter: (value) => filter(channel, value),
state: legend.attributes.state,
channel,
emitter,
});
const channels = dataOf(legend).scales.map((d) => d.name);
if (legend.className === CATEGORY_LEGEND_CLASS_NAME) {
return legendFilterOrdinal(container, {
legends: itemsOf,
marker: markerOf,
label: labelOf,
datum: (d) => {
const { __data__: datum } = d;
const { index } = datum;
return domain[index];
},
filter: (value) => filter(channel, value, true, channels),
state: legend.attributes.state,
channel,
emitter,
});
} else {
return legendFilterContinuous(container, {
legend,
filter: (value) => filter(channel, value, false, channels),
emitter,
channel,
});
}
});
return () => {
removes.forEach((remove) => remove());
Expand Down
Loading