Skip to content

feat: add new api filterGraphicsByDatum to BaseChart #3879

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 2 commits into from
Apr 11, 2025
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
142 changes: 92 additions & 50 deletions packages/vchart/src/chart/base/base-chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1226,65 +1226,107 @@ export class BaseChart<T extends IChartSpec> extends CompilableBase implements I
});
}

protected _setStateInDatum(
stateKey: string,
checkReverse: boolean,
filterGraphicsByDatum(
datum: MaybeArray<Datum> | null,
filter?: (series: ISeries, mark: IMark) => boolean,
region?: IRegionQuerier
opt: {
filter?: (series: ISeries, mark: IMark) => boolean;
region?: IRegionQuerier;
getDatum?: (el: IElement, mark: IMark, s: ISeries, r: IRegion) => Datum;
callback?: (el: IElement, mark: IMark, s: ISeries, r: IRegion) => void;
regionCallback?: (pickElements: IElement[], r: IRegion) => void;
} = {}
) {
datum = datum ? array(datum) : null;
const keys = !datum ? null : Object.keys(datum[0]);
this.getRegionsInQuerier(region).forEach(r => {
if (!datum) {
r.interaction.clearEventElement(stateKey, true);
return;
}
r.getSeries().forEach(s => {
s.getMarks().forEach(m => {
if (!m.getProduct()) {
return;
}
if (!filter || (isFunction(filter) && filter(s, m))) {
const isCollect = m.getProduct().isCollectionMark();
const elements = m.getProduct().elements;
let pickElements = [] as IElement[];
if (isCollect) {
pickElements = elements.filter(e => {
const elDatum = e.getDatum();
// eslint-disable-next-line max-nested-callbacks, eqeqeq
(datum as Datum[]).every((d, index) => keys.every(k => d[k] == elDatum[index][k]));
});
} else {
if (datum.length > 1) {
const datumTemp = (datum as Datum[]).slice();
pickElements = elements.filter(e => {
if (datumTemp.length === 0) {
return false;
}
const elDatum = e.getDatum();
// eslint-disable-next-line max-nested-callbacks, eqeqeq
const index = datumTemp.findIndex(d => keys.every(k => d[k] == elDatum[k]));
if (index >= 0) {
datumTemp.splice(index, 1);
return true;
const keys = !datum ? null : Object.keys((datum as Datum[])[0]);
const allElements = [] as IElement[];
const getDatumOfElement = opt.getDatum ?? ((el: IElement) => el.getDatum());

this.getRegionsInQuerier(opt.region).forEach(r => {
const pickElements = [] as IElement[];
datum &&
r.getSeries().forEach(s => {
s.getMarks().forEach(m => {
if (!m.getProduct()) {
return;
}
if (!opt.filter || (isFunction(opt.filter) && opt.filter(s, m))) {
const isCollect = m.getProduct().isCollectionMark();
const elements = m.getProduct().elements;
if (isCollect) {
elements.filter(e => {
const elDatum = getDatumOfElement(e, m, s, r);
const isPick =
// eslint-disable-next-line max-nested-callbacks, eqeqeq
elDatum && (datum as Datum[]).every((d, index) => keys.every(k => d[k] == elDatum[index][k]));

if (isPick) {
pickElements.push(e);
allElements.push(e);
opt.callback && opt.callback(e, m, s, r);
}
return false;
});
} else {
// eslint-disable-next-line eqeqeq
const el = elements.find(e => keys.every(k => datum[0][k] == e.getDatum()[k]));
el && (pickElements = [el]);
if (datum.length > 1) {
const datumTemp = (datum as Datum[]).slice();

elements.forEach(e => {
const elDatum = getDatumOfElement(e, m, s, r);
// eslint-disable-next-line max-nested-callbacks, eqeqeq
const index = elDatum && datumTemp.findIndex(d => keys.every(k => d[k] == elDatum[k]));
if (index >= 0) {
datumTemp.splice(index, 1);

pickElements.push(e);
allElements.push(e);
opt.callback && opt.callback(e, m, s, r);
}
});
} else {
const el = elements.find(e => {
const elDatum = getDatumOfElement(e, m, s, r);
// eslint-disable-next-line eqeqeq
return elDatum && keys.every(k => (datum as Datum[])[0][k] == elDatum[k]);
});

if (el) {
pickElements.push(el);
allElements.push(el);
opt.callback && opt.callback(el, m, s, r);
}
}
}
}
pickElements.forEach(element => {
r.interaction.startInteraction(stateKey, element);
});
}
});
});
});
if (checkReverse) {
r.interaction.reverseEventElement(stateKey);

opt.regionCallback && opt.regionCallback(pickElements, r);
});

return allElements;
}

protected _setStateInDatum(
stateKey: string,
checkReverse: boolean,
datum: MaybeArray<Datum> | null,
filter?: (series: ISeries, mark: IMark) => boolean,
region?: IRegionQuerier
) {
this.filterGraphicsByDatum(datum, {
filter,
region,
regionCallback: (elements, r) => {
if (!datum) {
r.interaction.clearEventElement(stateKey, true);
} else if (elements.length) {
elements.forEach(e => {
r.interaction.startInteraction(stateKey, e);
});

if (checkReverse) {
r.interaction.reverseEventElement(stateKey);
}
}
}
});
}
Expand Down
16 changes: 15 additions & 1 deletion packages/vchart/src/chart/interface/chart.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { IEvent } from '../../event/interface';
import type { LayoutCallBack } from '../../layout/interface';
import type { IView } from '@visactor/vgrammar-core';
import type { IElement, IView } from '@visactor/vgrammar-core';
import type { IParserOptions } from '@visactor/vdataset';
import type { IComponent, IComponentConstructor } from '../../component/interface';
import type { IMark } from '../../mark/interface';
Expand Down Expand Up @@ -210,6 +210,20 @@ export interface IChart extends ICompilable {
getSeriesData: (id: StringOrNumber | undefined, index: number | undefined) => DataView | undefined;
// setDimensionIndex
setDimensionIndex: (value: StringOrNumber, opt: DimensionIndexOption) => void;
/**
* 根据数据筛选图元
* @since 1.13.9
*/
filterGraphicsByDatum: (
datum: MaybeArray<Datum> | null,
opt?: {
filter?: (series: ISeries, mark: IMark) => boolean;
region?: IRegionQuerier;
getDatum?: (el: IElement, mark: IMark, s: ISeries, r: IRegion) => Datum;
callback?: (el: IElement, mark: IMark, s: ISeries, r: IRegion) => void;
regionCallback?: (pickElements: IElement[], r: IRegion) => void;
}
) => IElement[];
}

export interface IChartSpecTransformer {
Expand Down
78 changes: 31 additions & 47 deletions packages/vchart/src/chart/sankey/sankey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { Datum, MaybeArray } from '../../typings/common';
import type { ISeries } from '../../series/interface';
import type { IMark } from '../../mark/interface/common';
import type { IRegionQuerier } from '../../typings/params';
import { isArray, isFunction } from '@visactor/vutils';
import { isArray } from '@visactor/vutils';
import { loadScrollbar } from '@visactor/vrender-components';

export class SankeyChart<T extends ISankeyChartSpec = ISankeyChartSpec> extends BaseChart<T> {
Expand All @@ -29,57 +29,41 @@ export class SankeyChart<T extends ISankeyChartSpec = ISankeyChartSpec> extends
) {
// 桑基图暂时只支持单选
const activeDatum = isArray(datum) ? datum[0] : datum;
const keys = !activeDatum ? null : Object.keys(activeDatum);
this.getRegionsInQuerier(region).forEach(r => {
if (!activeDatum) {
r.interaction.clearEventElement(stateKey, true);
return;
}
let hasPick = false;
r.getSeries().forEach(s => {
let activeNodeOrLink = null;

s.getMarksWithoutRoot().forEach(m => {
if (m.type === 'text') {
return;
}

let pickElement = null;
const mark = m.getProduct();
if (!mark) {
return;
}
if (!filter || (isFunction(filter) && filter(s, m))) {
pickElement = mark.elements.find((e: any) =>
keys.every(k => {
let datum = e.getDatum()?.datum;
const markFilter = (series: ISeries, mark: IMark) => {
return mark.type !== 'text' && mark.getProduct() && (!filter || filter(series, mark));
};

if (isArray(datum)) {
// data of link
datum = datum[0];
}
this.filterGraphicsByDatum(activeDatum, {
filter: markFilter,
region,
getDatum: e => {
let d = e.getDatum()?.datum;

// eslint-disable-next-line eqeqeq
return activeDatum[k] == datum?.[k];
})
);
}
if (pickElement) {
hasPick = true;
r.interaction.startInteraction(stateKey, pickElement);
if (isArray(d)) {
// data of link
d = d[0];
}
return d;
},
callback: (element, mark, s, r) => {
const id = mark.getProduct()?.id();
if (id && (id.includes('node') || id.includes('link'))) {
(s as any)._handleEmphasisElement?.({ item: element });
}
},
regionCallback: (elements, r) => {
if (!activeDatum) {
r.interaction.clearEventElement(stateKey, true);
return;
} else if (elements.length) {
elements.forEach(e => {
r.interaction.startInteraction(stateKey, e);
});

if (mark.id().includes('node') || mark.id().includes('link')) {
activeNodeOrLink = pickElement;
}
if (checkReverse) {
r.interaction.reverseEventElement(stateKey);
}
});

if (activeNodeOrLink) {
(s as any)._handleEmphasisElement?.({ item: activeNodeOrLink });
}
});
if (checkReverse && hasPick) {
r.interaction.reverseEventElement(stateKey);
}
});
}
Expand Down
Loading