Skip to content

Commit

Permalink
feat(venn): 初始化韦恩图 (#2831)
Browse files Browse the repository at this point in the history
* feat(venn): 初始化韦恩图

* feat(venn): 韦恩图支持 meta 设置

* refactor(venn): 重构韦恩图的color字段

* feat(venn): 韦恩图颜色叠加问题

* docs(venn): 补充韦恩图 demo

* feat(venn): 韦恩图抽取一些字段常量 & 增加图例配置

* feat(venn): 韦恩图默认配置项

* feat(venn): 处理下韦恩图布局 padding 等问题

* fix(venn): 修复韦恩图绘制问题,G 对小写 a 绘制会有问题

* feat(venn): 韦恩图 tooltip 设置

* feat(venn): 韦恩图支持 label 展示

* docs(venn): 丰富韦恩图 demo

* docs(venn): 丰富韦恩图 tooltip 展示 demo

* refactor(venn): 韦恩图布局相关的算法从 js 文件改造为 ts 文件

* test(venn): 添加韦恩图基础单测

* fix(venn): 修复韦恩图布局大小问题 & 添加单测

* chore: 包大小又增加了

* fix(venn): 修复韦恩图单测错误

* test(venn): 补充单测

* refactor(color-blend): 重构韦恩图颜色叠加工具方法

* docs(venn): 韦恩图 demo 移除 appendPadding 设置,内置为默认配置项

* feat(venn): 韦恩图增加 sizeField 和 setsField

* test(utils): 补充单测

* test: venn图的基础配置的单测和文档 (#2836)

* test: venn图的基础配置的单测和文档

* refactor: 处理lint警告,删除暂无用变量

* test: 补充和修改单测的写法

Co-authored-by: 酥云 <lisuwen.lsw@antgroup.com>

* refactor(padding): venn图增加padding配置项 (#2838)

* refactor(padding): venn图增加padding配置项

* refactor(padding): 把resolveAllPadding方法抽成公共utils,重新处理venn的padding

* refactor(padding): 去掉多余的normalPadding方法

Co-authored-by: 酥云 <lisuwen.lsw@antgroup.com>

* refactor(blend): 优化颜色叠加计算 (#2837)

* refactor(blend): 优化颜色叠加计算

* test(blend): 补充blend的单测

* refactor: 调整打包限制的大小

* refactor(blend): 删除nameToHex工具,使用color-utils

Co-authored-by: 酥云 <lisuwen.lsw@antgroup.com>

Co-authored-by: Susan <527971893@qq.com>
Co-authored-by: 酥云 <lisuwen.lsw@antgroup.com>
  • Loading branch information
3 people authored Sep 9, 2021
1 parent d1e1a52 commit e2b84c6
Show file tree
Hide file tree
Showing 39 changed files with 3,008 additions and 30 deletions.
2 changes: 0 additions & 2 deletions __tests__/unit/plots/tiny-area/pattern-spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { TooltipCfg } from '@antv/g2/lib/interface';
import { TinyArea } from '../../../../src';
import { DEFAULT_OPTIONS } from '../../../../src/plots/tiny-area/constants';
import { partySupport } from '../../../data/party-support';
import { createDiv } from '../../../utils/dom';

Expand Down
114 changes: 114 additions & 0 deletions __tests__/unit/plots/venn/blend-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Venn } from '../../../../src';
import { createDiv } from '../../../utils/dom';

describe('venn: blendMode', () => {
const plot = new Venn(createDiv(), {
data: [
{ sets: ['A'], size: 12, label: 'A' },
{ sets: ['B'], size: 12, label: 'B' },
{ sets: ['C'], size: 12, label: 'C' },
{ sets: ['A', 'B'], size: 2, label: 'A&B' },
{ sets: ['A', 'C'], size: 2, label: 'A&C' },
{ sets: ['B', 'C'], size: 2, label: 'B&C' },
],
width: 400,
height: 500,
setsField: 'sets',
sizeField: 'size',
color: ['red', 'lime', 'blue'],
legend: false,
});
plot.render();

it('blendMode: default multiply', () => {
plot.update({ blendMode: 'multiply' });

expect(plot.chart.geometries[0].elements[0].getModel().color).toBe('red');
expect(plot.chart.geometries[0].elements[1].getModel().color).toBe('lime');
expect(plot.chart.geometries[0].elements[2].getModel().color).toBe('blue');
expect(plot.chart.geometries[0].elements[3].getModel().color).toBe('rgba(0, 0, 0, 1)'); // 交集元素
expect(plot.chart.geometries[0].elements[4].getModel().color).toBe('rgba(0, 0, 0, 1)');
expect(plot.chart.geometries[0].elements[5].getModel().color).toBe('rgba(0, 0, 0, 1)');
});

it('blendMode: normal', () => {
plot.update({ blendMode: 'normal' });

expect(plot.chart.geometries[0].elements[0].getModel().color).toBe('red');
expect(plot.chart.geometries[0].elements[1].getModel().color).toBe('lime');
expect(plot.chart.geometries[0].elements[2].getModel().color).toBe('blue');
expect(plot.chart.geometries[0].elements[3].getModel().color).toBe('rgba(255, 0, 0, 1)'); // 交集元素
expect(plot.chart.geometries[0].elements[4].getModel().color).toBe('rgba(255, 0, 0, 1)');
expect(plot.chart.geometries[0].elements[5].getModel().color).toBe('rgba(0, 255, 0, 1)');
});

it('blendMode: darken', () => {
plot.update({ blendMode: 'darken' });

expect(plot.chart.geometries[0].elements[0].getModel().color).toBe('red');
expect(plot.chart.geometries[0].elements[1].getModel().color).toBe('lime');
expect(plot.chart.geometries[0].elements[2].getModel().color).toBe('blue');
expect(plot.chart.geometries[0].elements[3].getModel().color).toBe('rgba(0, 0, 0, 1)'); // 交集元素
expect(plot.chart.geometries[0].elements[4].getModel().color).toBe('rgba(0, 0, 0, 1)');
expect(plot.chart.geometries[0].elements[5].getModel().color).toBe('rgba(0, 0, 0, 1)');
});

it('blendMode: lighten', () => {
plot.update({ blendMode: 'lighten' });

expect(plot.chart.geometries[0].elements[0].getModel().color).toBe('red');
expect(plot.chart.geometries[0].elements[1].getModel().color).toBe('lime');
expect(plot.chart.geometries[0].elements[2].getModel().color).toBe('blue');
expect(plot.chart.geometries[0].elements[3].getModel().color).toBe('rgba(255, 255, 0, 1)'); // 交集元素
expect(plot.chart.geometries[0].elements[4].getModel().color).toBe('rgba(255, 0, 255, 1)');
expect(plot.chart.geometries[0].elements[5].getModel().color).toBe('rgba(0, 255, 255, 1)');
});

it('blendMode: screen', () => {
plot.update({ blendMode: 'screen' });

expect(plot.chart.geometries[0].elements[0].getModel().color).toBe('red');
expect(plot.chart.geometries[0].elements[1].getModel().color).toBe('lime');
expect(plot.chart.geometries[0].elements[2].getModel().color).toBe('blue');
expect(plot.chart.geometries[0].elements[3].getModel().color).toBe('rgba(255, 255, 0, 1)'); // 交集元素
expect(plot.chart.geometries[0].elements[4].getModel().color).toBe('rgba(255, 0, 255, 1)');
expect(plot.chart.geometries[0].elements[5].getModel().color).toBe('rgba(0, 255, 255, 1)');
});

it('blendMode: overlay', () => {
plot.update({ blendMode: 'overlay' });

expect(plot.chart.geometries[0].elements[0].getModel().color).toBe('red');
expect(plot.chart.geometries[0].elements[1].getModel().color).toBe('lime');
expect(plot.chart.geometries[0].elements[2].getModel().color).toBe('blue');
expect(plot.chart.geometries[0].elements[3].getModel().color).toBe('rgba(0, 255, 0, 1)'); // 交集元素
expect(plot.chart.geometries[0].elements[4].getModel().color).toBe('rgba(0, 0, 255, 1)');
expect(plot.chart.geometries[0].elements[5].getModel().color).toBe('rgba(0, 0, 255, 1)');
});

it('blendMode: burn', () => {
plot.update({ blendMode: 'burn' });

expect(plot.chart.geometries[0].elements[0].getModel().color).toBe('red');
expect(plot.chart.geometries[0].elements[1].getModel().color).toBe('lime');
expect(plot.chart.geometries[0].elements[2].getModel().color).toBe('blue');
expect(plot.chart.geometries[0].elements[3].getModel().color).toBe('rgba(0, 255, 0, 1)'); // 交集元素
expect(plot.chart.geometries[0].elements[4].getModel().color).toBe('rgba(0, 0, 255, 1)');
expect(plot.chart.geometries[0].elements[5].getModel().color).toBe('rgba(0, 0, 255, 1)');
});

it('blendMode: dodge', () => {
plot.update({ blendMode: 'dodge' });

expect(plot.chart.geometries[0].elements[0].getModel().color).toBe('red');
expect(plot.chart.geometries[0].elements[1].getModel().color).toBe('lime');
expect(plot.chart.geometries[0].elements[2].getModel().color).toBe('blue');
expect(plot.chart.geometries[0].elements[3].getModel().color).toBe('rgba(255, 255, 0, 1)'); // 交集元素
expect(plot.chart.geometries[0].elements[4].getModel().color).toBe('rgba(255, 0, 255, 1)');
expect(plot.chart.geometries[0].elements[5].getModel().color).toBe('rgba(0, 255, 255, 1)');
});

afterAll(() => {
plot.destroy();
});
});
63 changes: 63 additions & 0 deletions __tests__/unit/plots/venn/color-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Venn } from '../../../../src';
import { createDiv } from '../../../utils/dom';

describe('venn: color', () => {
const plot = new Venn(createDiv(), {
data: [
{ sets: ['A'], size: 12, label: 'A' },
{ sets: ['B'], size: 12, label: 'B' },
{ sets: ['C'], size: 12, label: 'C' },
{ sets: ['A', 'B'], size: 2, label: 'A&B' },
{ sets: ['A', 'C'], size: 2, label: 'A&C' },
{ sets: ['B', 'C'], size: 2, label: 'B&C' },
],
width: 400,
height: 500,
setsField: 'sets',
sizeField: 'size',
// default blendMode: 'multiply',
legend: false,
});
plot.render();

it('color: string', () => {
plot.update({ color: 'red' });

expect(plot.chart.geometries[0].elements[0].getModel().color).toBe('red');
expect(plot.chart.geometries[0].elements[1].getModel().color).toBe('red');
expect(plot.chart.geometries[0].elements[2].getModel().color).toBe('red');
expect(plot.chart.geometries[0].elements[3].getModel().color).toBe('rgba(255, 0, 0, 1)'); // 交集元素
expect(plot.chart.geometries[0].elements[4].getModel().color).toBe('rgba(255, 0, 0, 1)');
expect(plot.chart.geometries[0].elements[5].getModel().color).toBe('rgba(255, 0, 0, 1)');
});

it('color: array', () => {
plot.update({ color: ['red', 'blue', 'yellow'] });

expect(plot.chart.geometries[0].elements[0].getModel().color).toBe('red');
expect(plot.chart.geometries[0].elements[1].getModel().color).toBe('blue');
expect(plot.chart.geometries[0].elements[2].getModel().color).toBe('yellow');
expect(plot.chart.geometries[0].elements[3].getModel().color).toBe('rgba(0, 0, 0, 1)'); // 交集元素
expect(plot.chart.geometries[0].elements[4].getModel().color).toBe('rgba(255, 0, 0, 1)');
expect(plot.chart.geometries[0].elements[5].getModel().color).toBe('rgba(0, 0, 0, 1)');
});

it('color: callback', () => {
plot.update({
color: ({ size }) => {
return size > 2 ? 'red' : 'blue';
},
});

expect(plot.chart.geometries[0].elements[0].getModel().color).toBe('red');
expect(plot.chart.geometries[0].elements[1].getModel().color).toBe('red');
expect(plot.chart.geometries[0].elements[2].getModel().color).toBe('red');
expect(plot.chart.geometries[0].elements[3].getModel().color).toBe('blue');
expect(plot.chart.geometries[0].elements[4].getModel().color).toBe('blue');
expect(plot.chart.geometries[0].elements[5].getModel().color).toBe('blue');
});

afterAll(() => {
plot.destroy();
});
});
32 changes: 32 additions & 0 deletions __tests__/unit/plots/venn/data-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Venn } from '../../../../src';
import { createDiv } from '../../../utils/dom';

describe('venn 异常分支处理', () => {
const plot = new Venn(createDiv(), {
width: 400,
height: 500,
setsField: 'sets',
sizeField: 'size',
data: [],
});

it('并集中,出现不存在的集合', () => {
function render() {
plot.changeData([
{ sets: ['A'], size: 12, label: 'A' },
{ sets: ['B'], size: 12, label: 'B' },
{ sets: ['C'], size: 12, label: 'C' },
{ sets: ['A', 'B'], size: 2, label: 'A&B' },
{ sets: ['A', 'C'], size: 2, label: 'A&C' },
{ sets: ['B', 'D'], size: 2, label: 'B&C' },
{ sets: ['A', 'B', 'C'], size: 300 },
]);
}
// 下个 pr 再处理
expect(render).toThrowError();
});

afterAll(() => {
plot.destroy();
});
});
144 changes: 144 additions & 0 deletions __tests__/unit/plots/venn/index-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { IGroup } from '@antv/g-base';
import { Venn } from '../../../../src';
import { DEFAULT_OPTIONS } from '../../../../src/plots/venn/constant';
import { VennData } from '../../../../src/plots/venn/types';
import { createDiv } from '../../../utils/dom';

describe('venn', () => {
const data1 = [{ sets: ['A'], size: 10, label: 'A' }];

const plot = new Venn(createDiv(), {
width: 400,
height: 500,
setsField: 'sets',
sizeField: 'size',
data: data1,
appendPadding: 0,
legend: false,
});

it('sets: 1个集合', () => {
plot.render();
expect(plot.type).toBe('venn');
// @ts-ignore
expect(plot.getDefaultOptions()).toEqual(Venn.getDefaultOptions());

// VennShape 已注册
const elements = plot.chart.geometries[0].elements;
expect(elements[0].shapeFactory['venn']).toBeDefined();

expect(elements.length).toBe(1);
expect((elements[0].getData() as any).size).toBe(10);
// path: [['M', ...], ['A', rx ry 0 1 0 x y], ...]
const path = (elements[0].shape as IGroup).getChildren()[0].attr('path');
const label = (elements[0].shape as IGroup).getChildren()[1];
expect(label.get('name')).toBe('venn-label');
expect(path[0][0]).toBe('M');
expect(path[1][0]).toBe('A');
expect(path[2][0]).toBe('A');
expect((elements[0].getData() as any).radius).toBe(400 / 2);
expect(path[1][1]).toBe(400 / 2);
});

it('sets: 2个集合 & changedata', () => {
plot.changeData([
{ sets: ['A'], size: 10, label: 'A' },
{ sets: ['B'], size: 10, label: 'B' },
]);

const elements = plot.chart.geometries[0].elements;

expect(elements.length).toBe(2);
expect(elements[0].getModel().size).toBe(1);
expect(elements[1].getModel().size).toBe(1);
// path: [['M', ...], ['A', rx ry 0 1 0 x y], ...]
const path = (elements[0].shape as IGroup).getChildren()[0].attr('path');
const label = (elements[0].shape as IGroup).getChildren()[1];
expect(label.get('name')).toBe('venn-label');
expect(path[0][0]).toBe('M');
expect(path[1][0]).toBe('A');
expect(path[2][0]).toBe('A');
expect(path[1][1]).toBe(400 / 2 / 2);
expect(path[1][6]).toBe(200);
// 居中
expect(path[1][7]).toBe(500 / 2);

const path1 = (elements[1].shape as IGroup).getChildren()[0].attr('path');
expect(path1[1][1]).toBe(400 / 2 / 2);
expect(path1[1][6]).toBe(400);
expect(path1[1][7]).toBe(500 / 2);
});

it('sets: 2个集合 & exist intersection', () => {
plot.changeData([
{ sets: ['A'], size: 10, label: 'A' },
{ sets: ['B'], size: 10, label: 'B' },
{ sets: ['A', 'B'], size: 4, label: 'A&B' },
]);

const elements = plot.chart.geometries[0].elements;

expect(elements.length).toBe(3);
// 📒 size 的具体计算逻辑,可以再看下
expect((elements[0].getData() as any).size).toBe(10);
expect((elements[1].getData() as any).size).toBe(10);
expect((elements[2].getData() as any).size).toBe(4);
// path: [['M', ...], ['A', rx ry 0 1 0 x y], ...]
const path = (elements[0].shape as IGroup).getChildren()[0].attr('path');
const label = (elements[0].shape as IGroup).getChildren()[1];
expect(label.get('name')).toBe('venn-label');
expect(path[0][0]).toBe('M');
expect(path[1][0]).toBe('A');
expect(path[2][0]).toBe('A');
expect(path[1][1]).toBeGreaterThan(400 / 2 / 2);
// 有交集,所以往前进一步
expect(path[1][6]).toBeGreaterThan(200);
// 居中
expect(path[1][7]).toBeCloseTo(500 / 2);

const path1 = (elements[1].shape as IGroup).getChildren()[0].attr('path');
expect(path1[1][1]).toBeGreaterThan(400 / 2 / 2);
expect(path1[1][6]).toBeLessThan(400);
expect(path1[1][7]).toBeCloseTo(500 / 2);
});

it('sets: 3个集合 & exist intersection', () => {
plot.changeData([
{ sets: ['A'], size: 12, label: 'A' },
{ sets: ['B'], size: 12, label: 'B' },
{ sets: ['C'], size: 12, label: 'C' },
{ sets: ['A', 'B'], size: 2, label: 'A&B' },
{ sets: ['A', 'C'], size: 2, label: 'A&C' },
{ sets: ['B', 'C'], size: 2, label: 'B&C' },
{ sets: ['A', 'B', 'C'], size: 1 },
]);

const elements = plot.chart.geometries[0].elements;

expect(elements.length).toBe(7);
const path = (elements[0].shape as IGroup).getChildren()[0].attr('path');

expect(path[0][0]).toBe('M');
expect(path[1][0]).toBe('A');
expect(path[2][0]).toBe('A');

// 中心点
expect((elements[6].getData() as any).x).toBeCloseTo(200);
// 元素对齐
expect(elements[2].getData().x).toBeCloseTo((elements[6].getData() as any).x);
expect(elements[0].getData().y).toBeCloseTo((elements[1].getData() as any).y);
});

it('defaultOptions 保持从 constants 中获取', () => {
expect(Venn.getDefaultOptions()).toEqual(DEFAULT_OPTIONS);
});

it('韦恩图数据结构类型定义 types & constants', () => {
const vennData: VennData = [{ sets: [], size: 0, path: '', id: '' }];
expect(vennData).toBeDefined();
});

afterAll(() => {
plot.destroy();
});
});
Loading

0 comments on commit e2b84c6

Please sign in to comment.