Skip to content

Commit

Permalink
refactor(geometry): 优化 label 和 element 的绑定逻辑 (#3884)
Browse files Browse the repository at this point in the history
* refactor(geometry): 优化 label 和 element 绑定逻辑,降低复杂度

详细见: #3884

* fix(geometry): element 上挂载的 labelShape 是一个 IGroup.
  • Loading branch information
visiky committed Apr 20, 2022
1 parent dc58631 commit 64f1c3a
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 39 deletions.
42 changes: 16 additions & 26 deletions src/geometry/base.ts
Expand Up @@ -160,20 +160,6 @@ export interface GeometryCfg {
multiplePieWidthRatio?: number;
}

// 根据 elementId 查找对应的 label,因为有可能一个 element 对应多个 labels,所以在给 labels 打标识时做了处理
// 打标规则详见 ./label/base.ts#L263
function filterLabelsById(id: string, labelsMap: Record<string, IGroup>) {
const labels = [];
each(labelsMap, (label: IGroup, labelId: string) => {
const elementId = labelId.split(' ')[0];
if (elementId === id) {
labels.push(label);
}
});

return labels;
}

/**
* Geometry 几何标记基类,主要负责数据到图形属性的映射以及绘制逻辑。
*/
Expand Down Expand Up @@ -2098,21 +2084,25 @@ export default class Geometry<S extends ShapePoint = ShapePoint> extends Base {

// 将 label 同 element 进行关联
const labelsMap = geometryLabel.labelsRenderer.shapesMap;
each(this.elementsMap, (element: Element, id) => {
const labels = filterLabelsById(id, labelsMap); // element 实例同 label 进行绑定
if (labels.length) {
element.labelShape = labels;
for (let i = 0; i < labels.length; i++) {
const label = labels[i];
const labelChildren = label.getChildren();
for (let j = 0; j < labelChildren.length; j++) {
const child = labelChildren[j];
child.cfg.name = ['element', 'label'];
child.cfg.element = element;
}
// Store labels for every element.
const elementLabels = new Map<Element, Set<IGroup>>();
each(labelsMap, (labelGroup: IGroup, labelGroupId: string) => {
const labelChildren = labelGroup.getChildren();
for (let j = 0; j < labelChildren.length; j++) {
const labelShape = labelChildren[j];
const element = this.elementsMap[labelShape.get('elementId') || labelGroupId.split(' ')[0]];
if (element) {
labelShape.cfg.name = ['element', 'label'];
labelShape.cfg.element = element;
const labels = elementLabels.get(element) || new Set();
labels.add(labelGroup);
elementLabels.set(element, labels);
}
}
});
for (const [element, labels] of elementLabels.entries()) {
element.labelShape = [...labels];
}
}
/**
* 是否需要进行群组入场动画
Expand Down
4 changes: 2 additions & 2 deletions src/geometry/label/base.ts
Expand Up @@ -74,8 +74,8 @@ export default class GeometryLabel {
return items;
}

public render(mapppingArray: MappingDatum[], isUpdate: boolean = false) {
const labelItems = this.getLabelItems(mapppingArray);
public render(mappingArray: MappingDatum[], isUpdate: boolean = false) {
const labelItems = this.getLabelItems(mappingArray);
const labelsRenderer = this.getLabelsRenderer();
const shapes = this.getGeometryShapes();
// 渲染文本
Expand Down
91 changes: 80 additions & 11 deletions tests/unit/geometry/base-spec.ts
Expand Up @@ -202,11 +202,11 @@ describe('Geometry', () => {
},
});

geometry.label('temperature', (val) => {});
geometry.label('temperature', (val) => { });
expect(geometry.labelOption.callback).toBeInstanceOf(Function);
expect(geometry.labelOption.cfg).toBeUndefined();

geometry.label('temperature', (val) => {}, {
geometry.label('temperature', (val) => { }, {
type: 'base',
});
expect(geometry.labelOption.callback).toBeInstanceOf(Function);
Expand Down Expand Up @@ -843,31 +843,31 @@ describe('Geometry', () => {
});

chart.data(data);
const geometry = chart.interval().position('year*valye');
const geometry = chart.interval().position('year*value');

const beforFn = jest.fn();
const beforeFn = jest.fn();
const afterFn = jest.fn();

// 无动画
geometry.animate(false);
geometry.once(GEOMETRY_LIFE_CIRCLE.BEFORE_DRAW_ANIMATE, () => beforFn(1));
geometry.once(GEOMETRY_LIFE_CIRCLE.BEFORE_DRAW_ANIMATE, () => beforeFn(1));
geometry.once(GEOMETRY_LIFE_CIRCLE.AFTER_DRAW_ANIMATE, () => afterFn(1));
chart.render();

await delay(500);
await delay(100);

expect(beforFn).not.toBeCalled();
expect(beforeFn).not.toBeCalled();
expect(afterFn).not.toBeCalled();

// 有动画
geometry.animate(true);
geometry.once(GEOMETRY_LIFE_CIRCLE.BEFORE_DRAW_ANIMATE, () => beforFn(2));
geometry.once(GEOMETRY_LIFE_CIRCLE.BEFORE_DRAW_ANIMATE, () => beforeFn(2));
geometry.once(GEOMETRY_LIFE_CIRCLE.AFTER_DRAW_ANIMATE, () => afterFn(2));
chart.changeSize(300, 300);

await delay(500);
await delay(100);

expect(beforFn).toBeCalledWith(2);
expect(beforeFn).toBeCalledWith(2);
// expect(afterFn).toBeCalledWith(2);

const fn = jest.fn();
Expand All @@ -877,7 +877,7 @@ describe('Geometry', () => {
callback: fn,
},
});
geometry.once(GEOMETRY_LIFE_CIRCLE.BEFORE_DRAW_ANIMATE, () => beforFn(3));
geometry.once(GEOMETRY_LIFE_CIRCLE.BEFORE_DRAW_ANIMATE, () => beforeFn(3));
geometry.once(GEOMETRY_LIFE_CIRCLE.AFTER_DRAW_ANIMATE, () => afterFn(3));
chart.changeSize(400, 400);

Expand Down Expand Up @@ -918,4 +918,73 @@ describe('Geometry', () => {
expect(geometry1.zIndexReversed).toBe(false);
expect(geometry1.elements[0].shape.get('zIndex')).not.toBeGreaterThan(geometry1.elements[1].shape.get('zIndex'));
});

describe('geometry renderLabels. Bind labels to elements', () => {
const data = [
{ year: '1991', value: 15468, type: 'a' },
{ year: '1992', value: 16100, type: 'a' },
{ year: '1993', value: 15900, type: 'a' },
{ year: '1998', value: 32040, type: 'a' },
{ year: '1991', value: 5468, type: 'b' },
{ year: '1992', value: 6100, type: 'b' },
{ year: '1993', value: 5900, type: 'b' },
{ year: '1998', value: 22040, type: 'b' },
];

it('Geometry, with color mapping', async () => {
async function renderGeometry(type: string) {
const chart = new Chart({ container: createDiv(), width: 500, height: 400 });
chart.data(data);
chart.clear();
let geometry = chart[type]().position('year*value').color('type').label('value').animate(false);
chart.render();

await delay(100);
expect(geometry.elements.length).toBe(['line', 'area'].includes(type) ? 2 : data.length);
const element = geometry.elements[0];
expect(element.labelShape.length).toBe(['line', 'area'].includes(type) ? data.length / 2 : 1);
expect(element.labelShape[0].get('visible')).toBe(true);
element.changeVisible(false);
expect(element.labelShape[0].get('visible')).toBe(false);
const bbox = element.getBBox();

chart.clear();
geometry = chart[type]().position('year*value').color('type').label(false).animate(false);
chart.render();
// previous bbox contains labelBBox, so it not equal to current bbox.
expect(geometry.elements[0].getBBox()).not.toEqual(bbox);
}
await renderGeometry('line');
await renderGeometry('area');
await renderGeometry('interval');
});

it('Line Geometry, without color mapping', async () => {
async function renderGeometry(type: string) {
const chart = new Chart({ container: createDiv(), width: 500, height: 400 });
chart.data(data.filter(d => d.type === 'a'));
chart.clear();
let geometry = chart[type]().position('year*value').label('value').animate(false);
chart.render();

await delay(100);
const element = geometry.elements[0];
expect(element.labelShape.length).toBe(['line', 'area'].includes(type) ? data.length / 2 : 1);
expect(element.labelShape[0].get('visible')).toBe(true);
element.changeVisible(false);
expect(element.labelShape[0].get('visible')).toBe(false);
const bbox = element.getBBox();

chart.clear();
geometry = chart[type]().position('year*value').label(false).animate(false);
chart.render();
// previous bbox contains labelBBox, so it not equal to current bbox.
expect(geometry.elements[0].getBBox()).not.toEqual(bbox);
}

await renderGeometry('line');
await renderGeometry('area');
await renderGeometry('interval');
});
});
});
1 change: 1 addition & 0 deletions tsconfig.json
Expand Up @@ -10,6 +10,7 @@
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"downlevelIteration": true,
"lib": ["esnext", "dom"],
"types": ["jest"],
"resolveJsonModule": true,
Expand Down

0 comments on commit 64f1c3a

Please sign in to comment.