Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/vchart/src/chart/venn/venn-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export class VennChartSpecTransformer<T extends IVennChartSpec = IVennChartSpec>
const series: any = super._getDefaultSeriesSpec(spec, [
'categoryField',
'valueField',
'emptySetKey',
'circle',
'overlap',
'overlapLabel'
Expand Down
5 changes: 4 additions & 1 deletion packages/vchart/src/series/venn/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export interface IVennSeriesSpec extends ISeriesSpec, IAnimationSpec<VennMark, V
* 权重字段
*/
valueField: string;

/**
* 空集合标签
*/
emptySetKey?: string;
/**
* 圆形图元配置
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/vchart/src/series/venn/tooltip-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { getVennSeriesDataKey } from './util';

export class VennTooltipHelper extends BaseSeriesTooltipHelper {
dimensionTooltipTitleCallback = (datum: any) => {
return getVennSeriesDataKey(datum?.[this.series.getDimensionField()[0]]);
return getVennSeriesDataKey(datum?.[this.series.getDimensionField()[0]], this.series.getSpec().emptySetKey);
};

markTooltipKeyCallback = (datum: any) => {
return getVennSeriesDataKey(datum?.[this.series.getDimensionField()[0]]);
return getVennSeriesDataKey(datum?.[this.series.getDimensionField()[0]], this.series.getSpec().emptySetKey);
};
}
5 changes: 4 additions & 1 deletion packages/vchart/src/series/venn/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { array } from '@visactor/vutils';

export const getVennSeriesDataKey = (sets: string | string[]) => {
export const getVennSeriesDataKey = (sets: string | string[], emptysetKey?: string) => {
if (!sets || (Array.isArray(sets) && sets.length === 0)) {
return emptysetKey || 'others';
}
return array(sets).join(',');
};
81 changes: 55 additions & 26 deletions packages/vchart/src/series/venn/venn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export class VennSeries<T extends IVennSeriesSpec = IVennSeriesSpec> extends Bas

static readonly mark: SeriesMarkMap = vennSeriesMark;
static readonly builtInTheme = { venn };

static readonly transformerConstructor = VennSeriesSpecTransformer;
readonly transformerConstructor = VennSeriesSpecTransformer;

Expand Down Expand Up @@ -67,11 +66,21 @@ export class VennSeries<T extends IVennSeriesSpec = IVennSeriesSpec> extends Bas
return this._valueField;
}

protected _emptySetKey!: string;
getEmptySetKey() {
return this._emptySetKey;
}
setEmptySetKey(f: string): string {
this._emptySetKey = f;
return this._emptySetKey;
}

setAttrFromSpec(): void {
super.setAttrFromSpec();
this.setCategoryField(this._spec.categoryField ?? 'sets');
this.setValueField(this._spec.valueField ?? 'size');
this.setSeriesField(this._spec.seriesField ?? DEFAULT_DATA_KEY);
this.setEmptySetKey(this._spec.emptySetKey ?? 'others');
}

initData() {
Expand Down Expand Up @@ -210,7 +219,13 @@ export class VennSeries<T extends IVennSeriesSpec = IVennSeriesSpec> extends Bas
{
x: datum => (datum as IVennCircleDatum).labelX,
y: datum => (datum as IVennCircleDatum).labelY,
text: datum => getVennSeriesDataKey((datum as IVennCircleDatum).sets),
text: datum => {
const sets = (datum as IVennOverlapDatum).sets;
if (!sets || (Array.isArray(sets) && sets.length === 0)) {
return '';
}
return getVennSeriesDataKey(sets);
},
maxLineWidth: (datum: any) => {
const { x, radius, labelX } = datum as IVennCircleDatum;
const circleX0 = x - radius;
Expand All @@ -234,7 +249,10 @@ export class VennSeries<T extends IVennSeriesSpec = IVennSeriesSpec> extends Bas
{
x: datum => (datum as IVennOverlapDatum).labelX,
y: datum => (datum as IVennOverlapDatum).labelY,
text: datum => getVennSeriesDataKey((datum as IVennOverlapDatum).sets)
text: datum => {
const sets = (datum as IVennOverlapDatum).sets;
return getVennSeriesDataKey(sets);
}
},
STATE_VALUE_ENUM.STATE_NORMAL,
AttributeLevel.Series
Expand Down Expand Up @@ -310,7 +328,7 @@ export class VennSeries<T extends IVennSeriesSpec = IVennSeriesSpec> extends Bas
protected _getSeriesInfo(field: string, keys: string[]) {
const defaultShapeType = this.getDefaultShapeType();
return keys.map(originalKey => {
const dataKey = getVennSeriesDataKey(originalKey);
const dataKey = getVennSeriesDataKey(originalKey, this._emptySetKey);
return {
key: dataKey,
originalKey,
Expand All @@ -324,7 +342,7 @@ export class VennSeries<T extends IVennSeriesSpec = IVennSeriesSpec> extends Bas

getSeriesFieldValue(datum: Datum, seriesField?: string) {
const value = super.getSeriesFieldValue(datum, seriesField);
return getVennSeriesDataKey(value);
return getVennSeriesDataKey(value, this._emptySetKey);
}

legendSelectedFilter(component: ILegend, selectedKeys: StringOrNumber[]) {
Expand All @@ -339,27 +357,38 @@ export class VennSeries<T extends IVennSeriesSpec = IVennSeriesSpec> extends Bas
if (selectedKeys.length === originalLegendKeys.length) {
return selectedKeys;
}

// 找到缺失的项
const selectedFilter = {};
selectedKeys.forEach(s => {
selectedFilter[s] = true;
});
const disableKeys = originalLegendKeys.filter(key => !selectedFilter[getVennSeriesDataKey(key)]);

// 找到缺失的项的派生项(如 “A&B” 的派生项 “A&B&C”)
const derivedDisableKeys = originalLegendKeys.filter(key => {
if (disableKeys.includes(key)) {
return false;
}
return disableKeys.some(disableKey => array(disableKey).every(k => key.includes(k)));
});

// 将派生项从 selectedKeys 中移除
selectedKeys = selectedKeys.slice();
derivedDisableKeys.forEach(key => {
selectedKeys.splice(selectedKeys.indexOf(getVennSeriesDataKey(key)), 1);
});
const emptyKey = this._emptySetKey;

const hasEmpty = selectedKeys.includes(emptyKey);
const nonEmpty = selectedKeys.filter(key => key !== emptyKey);

if (nonEmpty.length > 0) {
// 过滤出非空的原始图例键
const validKeys = originalLegendKeys.filter(key => getVennSeriesDataKey(key, this._emptySetKey) !== emptyKey);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 请依据原始集合结构识别空集合,避免误伤普通 others 集合

这里通过名称是否等于 emptyKey 过滤原始图例键,会把合法的普通集合 ['others'] 也当作空集合处理,即使数据中根本没有 sets: []。

在配置了 categoryField: 'sets'、valueField: 'value'、seriesField: 'sets' 和可见图例的普通 Venn 图中,使用以下数据:

[
  { sets: ['others'], value: 8 },
  { sets: ['A'], value: 10 },
  { sets: ['others', 'A'], value: 4 }
]

渲染后取消选中 others,对应公共 API 调用:

chart.setLegendSelectedDataByIndex(0, ['A', 'others,A']);

预期隐藏 others 及依赖它的交集,仅保留 A。当前实现将 ['others'] 从 validKeys 中排除,导致交集未被禁用。布局收到缺少组成集合的交集后,在 greedyLayout 抛出 TypeError: Cannot read property 'size' of undefined。同一数据和操作在 develop 4a51ea6a20d5 对照中通过,属于本 PR 引入的回归。

麻烦根据原始 sets 是否为空来识别空集合,并区分空集合身份与显示名称,避免把普通集合名 others 隐式变为保留名称。请补充这一场景的回归测试。

// 找到缺失的项
const selectedFilter: Record<StringOrNumber, boolean> = {};
selectedKeys.forEach(s => {
selectedFilter[s] = true;
});
const disableKeys = validKeys.filter(key => !selectedFilter[getVennSeriesDataKey(key, this._emptySetKey)]);

// 找到缺失的项的派生项(如 “A&B” 的派生项 “A&B&C”)
const derivedDisableKeys = validKeys.filter(key => {
if (disableKeys.includes(key)) {
return false;
}
return disableKeys.some(disableKey => array(disableKey).every(k => key.includes(k)));
});

// 将派生项从 nonEmpty 中移除
selectedKeys = nonEmpty.slice();
derivedDisableKeys.forEach(key => {
selectedKeys.splice(selectedKeys.indexOf(getVennSeriesDataKey(key, this._emptySetKey)), 1);
});
}
if (hasEmpty) {
selectedKeys.push(emptyKey);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 请避免仅选空集合时重复追加选中项并修改入参

当 selectedKeys = ['others'] 时,nonEmpty.length === 0,因此不会执行上面的 selectedKeys = nonEmpty.slice()。这里继续向原数组追加 emptyKey,结果和调用方入参都会变为 ['others', 'others']。

可在配置了 categoryField: 'sets'、valueField: 'value'、seriesField: 'sets' 和可见图例的普通 Venn 图中复现:

// data.values
[
  { sets: [], value: 6 },
  { sets: ['A'], value: 8 }
];

// 渲染后仅选中空集合
const selected = ['others'];
chart.setLegendSelectedDataByIndex(0, selected);

预期仅保留空集合,且 selected 仍为 ['others']。实际选中项和入参都有两个 others,过滤后的数据仍包含 A:discreteLegendFilter 将“选中项数量等于全量图例数量”视为全选,直接返回全部数据。将 emptySetKey 配为“其他”也有同样结果。

麻烦在 Venn 筛选器中始终构造独立的结果数组,确保空集合项最多加入一次,并补充两项图例下的单选回归测试。根因是这里生成了重复的选中项,应在此处修正。

}
}
return selectedKeys;
}
Expand Down