From 82ad1ea30f3c8cc2c5a3e01d3d7ec05263db7412 Mon Sep 17 00:00:00 2001 From: 14 Date: Sat, 29 May 2021 00:48:18 +0800 Subject: [PATCH 01/18] =?UTF-8?q?feat(violin):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=B0=8F=E6=8F=90=E7=90=B4=E5=9B=BE=20(#2569)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(violin): 添加小提琴图 * fix(violin): demo 的数据放到 CDN * doc(violin): 更新截图 * fix(violin): x 轴字段用常量定义 * refactor(violin): 去除 simple-statistics 包依赖,减小体积 * fix(violin): 修复不响应用户 tooltip 选项的问题 * fix(violin): 修复小提琴图中箱线图四分位的计算问题 * fix(violin): smooth 和 hollow API 改为 shape, 重命名字段常量 * docs(violin): 优化 box.textMap 的文档和 Demo * feat(violin): 使用 geometry 通用 adaptor, 添加格式化支持 * fix(violin): 修复 shape 通道未映射的问题 Co-authored-by: 14 --- __tests__/data/violin.ts | 602 ++++++++++++++++++ __tests__/unit/plots/violin/box-spec.ts | 76 +++ .../unit/plots/violin/change-data-spec.ts | 30 + __tests__/unit/plots/violin/index-spec.ts | 48 ++ __tests__/unit/plots/violin/utils-spec.ts | 63 ++ .../unit/plots/violin/violin-shape-spec.ts | 40 ++ docs/api/plots/violin.en.md | 118 ++++ docs/api/plots/violin.zh.md | 121 ++++ examples/more-plots/violin/API.en.md | 1 + examples/more-plots/violin/API.zh.md | 1 + examples/more-plots/violin/demo/basic.ts | 14 + examples/more-plots/violin/demo/group.ts | 15 + examples/more-plots/violin/demo/meta.json | 40 ++ examples/more-plots/violin/demo/shape.ts | 15 + examples/more-plots/violin/demo/tooltip.ts | 36 ++ examples/more-plots/violin/index.en.md | 4 + examples/more-plots/violin/index.zh.md | 4 + package.json | 2 + src/adaptor/geometries/index.ts | 2 + src/adaptor/geometries/violin.ts | 44 ++ src/index.ts | 4 + src/plots/violin/adaptor.ts | 315 +++++++++ src/plots/violin/constant.ts | 49 ++ src/plots/violin/index.ts | 42 ++ src/plots/violin/types.ts | 51 ++ src/plots/violin/utils.ts | 90 +++ src/utils/transform/quantile.ts | 210 ++++++ 27 files changed, 2037 insertions(+) create mode 100644 __tests__/data/violin.ts create mode 100644 __tests__/unit/plots/violin/box-spec.ts create mode 100644 __tests__/unit/plots/violin/change-data-spec.ts create mode 100644 __tests__/unit/plots/violin/index-spec.ts create mode 100644 __tests__/unit/plots/violin/utils-spec.ts create mode 100644 __tests__/unit/plots/violin/violin-shape-spec.ts create mode 100644 docs/api/plots/violin.en.md create mode 100644 docs/api/plots/violin.zh.md create mode 100644 examples/more-plots/violin/API.en.md create mode 100644 examples/more-plots/violin/API.zh.md create mode 100644 examples/more-plots/violin/demo/basic.ts create mode 100644 examples/more-plots/violin/demo/group.ts create mode 100644 examples/more-plots/violin/demo/meta.json create mode 100644 examples/more-plots/violin/demo/shape.ts create mode 100644 examples/more-plots/violin/demo/tooltip.ts create mode 100644 examples/more-plots/violin/index.en.md create mode 100644 examples/more-plots/violin/index.zh.md create mode 100644 src/adaptor/geometries/violin.ts create mode 100644 src/plots/violin/adaptor.ts create mode 100644 src/plots/violin/constant.ts create mode 100644 src/plots/violin/index.ts create mode 100644 src/plots/violin/types.ts create mode 100644 src/plots/violin/utils.ts create mode 100644 src/utils/transform/quantile.ts diff --git a/__tests__/data/violin.ts b/__tests__/data/violin.ts new file mode 100644 index 0000000000..aaa76b8d8d --- /dev/null +++ b/__tests__/data/violin.ts @@ -0,0 +1,602 @@ +export const BASE_VIOLIN_DATA = [ + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.5 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.9 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.3 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.2 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.7 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.1 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.6 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.6 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.4 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.7 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.9 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.3 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.6 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 2.9 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.1 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.1 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.9 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.7 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.6 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.8 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.1 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.8 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.1 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.1 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.3 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.2 }, + { species: 'I. setosa', type: 'SepalWidth', value: 4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.8 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.4 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 4.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.7 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.4 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.3 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.9 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.3 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.5 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.3 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.7 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.8 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.7 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.3 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.8 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.7 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.4 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.7 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.6 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.6 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.5 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.7 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.3 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.9 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.8 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.6 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.4 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.6 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.5 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.2 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.2 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.6 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.2 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.7 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.6 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.1 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.8 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.4 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.1 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 4.1 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.2 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 4.2 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.1 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.9 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.2 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.2 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.3 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.5 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.1 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.6 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.9 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.3 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.4 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.3 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.3 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.5 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.3 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.3 }, + { species: 'I. setosa', type: 'SepalWidth', value: 2.3 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.3 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.2 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.4 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.6 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.6 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.5 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.4 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.9 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.8 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.3 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.8 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.6 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.8 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.1 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.2 }, + { species: 'I. setosa', type: 'SepalLength', value: 4.6 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.5 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.7 }, + { species: 'I. setosa', type: 'SepalLength', value: 5.3 }, + { species: 'I. setosa', type: 'PetalWidth', value: 0.2 }, + { species: 'I. setosa', type: 'PetalLength', value: 1.4 }, + { species: 'I. setosa', type: 'SepalWidth', value: 3.3 }, + { species: 'I. setosa', type: 'SepalLength', value: 5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.4 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.7 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.2 }, + { species: 'I. versicolor', type: 'SepalLength', value: 7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.2 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.4 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.9 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.1 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.9 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.6 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.8 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.8 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.6 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.7 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.3 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.3 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.4 }, + { species: 'I. versicolor', type: 'SepalLength', value: 4.9 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.6 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.9 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.4 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.9 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.7 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.2 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.2 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.9 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.2 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.4 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.7 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.9 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.1 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.6 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.9 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.4 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.1 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.1 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.7 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.8 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.2 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.2 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.9 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.5 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.8 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.8 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.2 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.9 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.8 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.1 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.9 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.5 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.3 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.2 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.7 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.8 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.1 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.3 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.9 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.4 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.4 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.4 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.8 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.8 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.8 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.7 }, + { species: 'I. versicolor', type: 'PetalLength', value: 5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.9 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.6 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.8 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.4 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.7 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.4 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.2 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.9 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.7 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.8 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.6 }, + { species: 'I. versicolor', type: 'PetalLength', value: 5.1 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.7 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.4 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.6 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.5 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.4 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.5 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.7 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3.1 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.3 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.1 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.5 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.2 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.6 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.4 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.6 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.1 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.2 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.6 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.8 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3.3 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.2 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.7 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.6 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.2 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.2 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 3 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.2 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.9 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.7 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.3 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.9 }, + { species: 'I. versicolor', type: 'SepalLength', value: 6.2 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.1 }, + { species: 'I. versicolor', type: 'PetalLength', value: 3 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.5 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.1 }, + { species: 'I. versicolor', type: 'PetalWidth', value: 1.3 }, + { species: 'I. versicolor', type: 'PetalLength', value: 4.1 }, + { species: 'I. versicolor', type: 'SepalWidth', value: 2.8 }, + { species: 'I. versicolor', type: 'SepalLength', value: 5.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.5 }, + { species: 'I. virginica', type: 'PetalLength', value: 6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.3 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.9 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.7 }, + { species: 'I. virginica', type: 'SepalLength', value: 5.8 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.1 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.9 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.1 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.9 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.3 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.2 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.8 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.5 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.1 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.6 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.7 }, + { species: 'I. virginica', type: 'PetalLength', value: 4.5 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.5 }, + { species: 'I. virginica', type: 'SepalLength', value: 4.9 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.3 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.9 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.3 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.8 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.5 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.5 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.6 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.2 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.2 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.5 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.9 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.3 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.7 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.4 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.1 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.5 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.8 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2 }, + { species: 'I. virginica', type: 'PetalLength', value: 5 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.5 }, + { species: 'I. virginica', type: 'SepalLength', value: 5.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.4 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 5.8 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.3 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.2 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.4 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.5 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.5 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.2 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.7 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.9 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.6 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.5 }, + { species: 'I. virginica', type: 'PetalLength', value: 5 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.2 }, + { species: 'I. virginica', type: 'SepalLength', value: 6 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.7 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.2 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.9 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2 }, + { species: 'I. virginica', type: 'PetalLength', value: 4.9 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 5.6 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.7 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 4.9 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.7 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.3 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.1 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.7 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.2 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.2 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 4.8 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.2 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 4.9 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.1 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.1 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.4 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.6 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.8 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.2 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.9 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.4 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.4 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.9 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.2 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.4 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.5 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.8 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.3 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.4 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.6 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.1 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 6.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 7.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.4 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.4 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.3 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.5 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.1 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.4 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 4.8 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.1 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.4 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.1 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.9 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.4 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.6 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.1 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.1 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.9 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.9 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.7 }, + { species: 'I. virginica', type: 'SepalLength', value: 5.8 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.9 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.2 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.8 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.5 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.7 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.2 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.7 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.9 }, + { species: 'I. virginica', type: 'PetalLength', value: 5 }, + { species: 'I. virginica', type: 'SepalWidth', value: 2.5 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.3 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.2 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.5 }, + { species: 'I. virginica', type: 'PetalWidth', value: 2.3 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.4 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3.4 }, + { species: 'I. virginica', type: 'SepalLength', value: 6.2 }, + { species: 'I. virginica', type: 'PetalWidth', value: 1.8 }, + { species: 'I. virginica', type: 'PetalLength', value: 5.1 }, + { species: 'I. virginica', type: 'SepalWidth', value: 3 }, + { species: 'I. virginica', type: 'SepalLength', value: 5.9 }, +]; diff --git a/__tests__/unit/plots/violin/box-spec.ts b/__tests__/unit/plots/violin/box-spec.ts new file mode 100644 index 0000000000..b02c151705 --- /dev/null +++ b/__tests__/unit/plots/violin/box-spec.ts @@ -0,0 +1,76 @@ +import { Violin } from '../../../../src'; +import { MIN_MAX_VIEW_ID, QUANTILE_VIEW_ID, MEDIAN_VIEW_ID } from '../../../../src/plots/violin/constant'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin', () => { + it('renders box views.', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + const minMaxView = violin.chart.views.find((view) => view.id === MIN_MAX_VIEW_ID); + const quantileView = violin.chart.views.find((view) => view.id === QUANTILE_VIEW_ID); + const medianView = violin.chart.views.find((view) => view.id === MEDIAN_VIEW_ID); + + expect(minMaxView.geometries[0].type).toBe('interval'); + expect(quantileView.geometries[0].type).toBe('interval'); + expect(medianView.geometries[0].type).toBe('point'); + + violin.destroy(); + }); + + it("should not render box views when 'box' set to false.", () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + box: false, + }); + + violin.render(); + const minMaxView = violin.chart.views.find((view) => view.id === MIN_MAX_VIEW_ID); + const quantileView = violin.chart.views.find((view) => view.id === QUANTILE_VIEW_ID); + const medianView = violin.chart.views.find((view) => view.id === MEDIAN_VIEW_ID); + + expect(minMaxView).toBeUndefined(); + expect(quantileView).toBeUndefined(); + expect(medianView).toBeUndefined(); + + violin.destroy(); + }); + + it('should not render box with custom textMap.', () => { + const textMap = { + max: '最大值', + min: '最小值', + median: '中位值', + q1: '上四分位点', + q3: '下四分位点', + }; + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + box: { + textMap, + }, + }); + + violin.render(); + + // @ts-ignore + expect(violin.options.box.textMap).toEqual(textMap); + + violin.destroy(); + }); +}); diff --git a/__tests__/unit/plots/violin/change-data-spec.ts b/__tests__/unit/plots/violin/change-data-spec.ts new file mode 100644 index 0000000000..5e150c3e17 --- /dev/null +++ b/__tests__/unit/plots/violin/change-data-spec.ts @@ -0,0 +1,30 @@ +import { group } from '@antv/util'; +import { Violin } from '../../../../src'; +import { VIOLIN_VIEW_ID } from '../../../../src/plots/violin/constant'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin change data', () => { + it('renders new violins when data changed', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + const g = violin.chart.views.find((view) => view.id === VIOLIN_VIEW_ID).geometries[0]; + expect(g.elements.length).toBe(group(BASE_VIOLIN_DATA, 'type').length); + + const newData = BASE_VIOLIN_DATA.filter((data) => data.type !== 'PetalWidth'); + + violin.changeData(newData); + const newG = violin.chart.views.find((view) => view.id === VIOLIN_VIEW_ID).geometries[0]; + expect(violin.options.data).toEqual(newData); + expect(newG.elements.length).toBe(group(newData, 'type').length); + + violin.destroy(); + }); +}); diff --git a/__tests__/unit/plots/violin/index-spec.ts b/__tests__/unit/plots/violin/index-spec.ts new file mode 100644 index 0000000000..fad3482448 --- /dev/null +++ b/__tests__/unit/plots/violin/index-spec.ts @@ -0,0 +1,48 @@ +import { group } from '@antv/util'; +import { Violin } from '../../../../src'; +import { VIOLIN_VIEW_ID } from '../../../../src/plots/violin/constant'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin', () => { + it("renders N violins, where N equals to xField's length.", () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + const g = violin.chart.views.find((view) => view.id === VIOLIN_VIEW_ID).geometries[0]; + + // 个数 + expect(g.elements.length).toBe(group(BASE_VIOLIN_DATA, 'type').length); + // 类型 + expect(g.type).toBe('violin'); + + violin.destroy(); + }); + + it("renders N violins, where N equals to (xField * sierisField)'s length.", () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + seriesField: 'species', + }); + + violin.render(); + const g = violin.chart.views.find((view) => view.id === VIOLIN_VIEW_ID).geometries[0]; + + // 个数 + expect(g.elements.length).toBe(group(BASE_VIOLIN_DATA, ['type', 'species']).length); + // 类型 + expect(g.type).toBe('violin'); + + violin.destroy(); + }); +}); diff --git a/__tests__/unit/plots/violin/utils-spec.ts b/__tests__/unit/plots/violin/utils-spec.ts new file mode 100644 index 0000000000..fcec23bd81 --- /dev/null +++ b/__tests__/unit/plots/violin/utils-spec.ts @@ -0,0 +1,63 @@ +import { toBeDeepCloseTo } from 'jest-matcher-deep-close-to'; +import { group } from '@antv/util'; +import { toBoxValue, PdfOptions, toViolinValue, transformViolinData } from '../../../../src/plots/violin/utils'; +import { DEFAULT_OPTIONS } from '../../../../src/plots/violin/constant'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; + +expect.extend({ toBeDeepCloseTo }); + +describe('violin utils', () => { + it('toBoxValue() works correctly.', () => { + // 顺序数据,可口算结果。 + const ordered = [-1, 2, 3, 4, 4.4, 5, 8, 9, 100]; + // 随机打乱,不应影响计算结果。 + const shuffled = ordered.slice().sort(() => Math.random() - 0.5); + expect(toBoxValue(shuffled)).toEqual({ + minMax: [-1, 100], + quantile: [3, 8], + median: [4.4], + }); + }); + + it('toViolinValue() works correctly.', () => { + const options: PdfOptions = { + min: -1, + max: 10, + size: 5, + width: 2, + }; + // 顺序数据,可笔算结果。 + const ordered = [-1, 2, 3, 4, 4.4, 5, 8, 9, 10]; + // 随机打乱,不应影响计算结果。 + const shuffled = ordered.slice().sort(() => Math.random() - 0.5); + // @ts-ignore + expect(toViolinValue(shuffled, options)).toBeDeepCloseTo( + { + violinY: [-1 + (11 / 4) * 0, -1 + (11 / 4) * 1, -1 + (11 / 4) * 2, -1 + (11 / 4) * 3, -1 + (11 / 4) * 4], + violinSize: [ + 0.18518518518518517, 0.287037037037037, 0.22222222222222218, 0.17592592592592587, 0.12962962962962957, + ], + }, + 0.0001 + ); + }); + + it('transformViolinData() results in correct length.', () => { + const dataWithoutSeries = transformViolinData({ + ...DEFAULT_OPTIONS, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + const dataWithSeries = transformViolinData({ + ...DEFAULT_OPTIONS, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + seriesField: 'species', + }); + + expect(dataWithoutSeries.length).toBe(group(BASE_VIOLIN_DATA, ['type']).length); + expect(dataWithSeries.length).toBe(group(BASE_VIOLIN_DATA, ['type', 'species']).length); + }); +}); diff --git a/__tests__/unit/plots/violin/violin-shape-spec.ts b/__tests__/unit/plots/violin/violin-shape-spec.ts new file mode 100644 index 0000000000..93b19c569e --- /dev/null +++ b/__tests__/unit/plots/violin/violin-shape-spec.ts @@ -0,0 +1,40 @@ +import { Violin } from '../../../../src'; +import { VIOLIN_VIEW_ID } from '../../../../src/plots/violin/constant'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +const getViolinShapeType = (violin: Violin) => { + const g = violin.chart.views.find((view) => view.id === VIOLIN_VIEW_ID).geometries[0]; + // @ts-ignore shapeType 是私有属性 + return g.elements[0].shapeType; +}; + +describe('violin', () => { + it('renders hollow/smooth violins determined by options.', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + // Defaults to { smooth: true, hollow: false } + violin.render(); + expect(getViolinShapeType(violin)).toBe('violin'); + + violin.update({ shape: 'smooth' }); + violin.render(); + expect(getViolinShapeType(violin)).toBe('smooth'); + + violin.update({ shape: 'hollow' }); + violin.render(); + expect(getViolinShapeType(violin)).toBe('hollow'); + + violin.update({ shape: 'hollow-smooth' }); + violin.render(); + expect(getViolinShapeType(violin)).toBe('hollow-smooth'); + + violin.destroy(); + }); +}); diff --git a/docs/api/plots/violin.en.md b/docs/api/plots/violin.en.md new file mode 100644 index 0000000000..e835a74757 --- /dev/null +++ b/docs/api/plots/violin.en.md @@ -0,0 +1,118 @@ +--- +title: Box +order: 31 +--- + +### Plot Container + +`markdown:docs/common/chart-options.en.md` + +### Data Mapping + +#### data + +**required** _array object_ + +Configure the data source. The data source is a collection of objects. For example:`[{ time: '1991',value: 20 }, { time: '1992',value: 20 }]`。 + +`markdown:docs/common/xy-field.en.md` + +#### seriesField + +**optional** _string_ + +Grouping field. It is used for grouping by default, and color is used as visual channel. +Outlier field. + +#### kde + +**optional** _object_ + +Options to generate Kernel Density Estimation. Currently only triangular kernel was supported. + +```ts +type KdeOptions = { + /** Triangular kernel */ + type: 'triangular'; + /** Min value for the kde's x range. Defaults to smallest value minus some threshold. */ + min?: number; + /** Max value for the kde's x range. Defaults to largest value plus some threshold. */ + max?: number; + /** Number of points to represent the kde. Defaults to 32. */ + sampleSize?: number; + /** Bandwith of the triangular kernel. Defaults to 3. */ + width?: number; +}; +``` + +#### meta + +`markdown:docs/common/meta.en.md` + +### Graphic Style + +#### shape + +**optional** _'smooth'|'hollow'|'hollow-smooth'_ + +The shape of violin geometry. Could be 'smooth', 'hollow' or 'hollow-smooth'. Defaults to rough, solid voilins. + +#### violinStyle + +**optional** _StyleAttr | Function_ + +Violin graphic style. + +`markdown:docs/common/shape-style.en.md` + +### box + +**optional** _false | object_ + +Options to render inner box plot. Set `false` to avoid rendering box plot. + +The statistical data of inner box plot includes: + +- max: The maximum value, rendered as the highest point in the box plot. +- min: The minimum value, rendered as the lowest point in the box plot. +- q3: The 3rd quartile, rendered as box top. +- q1: The 1st quartile, rendered as box bottom. +- median: The median value, rendered as a little circle. + +You can specify these texts in `box.textMap`. + +```ts +type BoxOptions = false | { + /** Text of the box plot. */ + textMap?: { + /** Max value label. */ + max: string; + /** Min value label. */ + min: string; + /** 1st quantile value label. */ + q1: string; + /** 3rd quantile value label. */ + q3: string; + /** Median value label. */ + median: string; + }; +}; +``` + +`markdown:docs/common/color.en.md` + +### Plot Components + +`markdown:docs/common/component.en.md` + +### Plot Event + +`markdown:docs/common/events.en.md` + +### Plot Method + +`markdown:docs/common/chart-methods.en.md` + +### Plot Theme + +`markdown:docs/common/theme.en.md` diff --git a/docs/api/plots/violin.zh.md b/docs/api/plots/violin.zh.md new file mode 100644 index 0000000000..6dce5057d0 --- /dev/null +++ b/docs/api/plots/violin.zh.md @@ -0,0 +1,121 @@ +--- +title: 小提琴图 +order: 31 +--- + +### 图表容器 + +`markdown:docs/common/chart-options.zh.md` + +### 数据映射 + +#### data + +**required** _array object_ + +设置图表数据源。数据源为对象集合,例如:`[{ time: '1991',value: 20 }, { time: '1992',value: 20 }]`。 + +`markdown:docs/common/xy-field.zh.md` + +#### seriesField + +**optional** _string_ + +分组拆分字段,默认是分组情况,颜色作为视觉通道。 + +#### kde + +**optional** _object_ + +计算小提琴轮廓线(核密度估计)的核函数算法配置。目前只支持三角核函数。 + +```ts +type KdeOptions = { + /** 三角波类型 */ + type: 'triangular'; + /** 最小值,默认为数据中的最小值减去一个固定的阈值。 */ + min?: number; + /** 最大值,默认为数据中的最大值加上一个固定的阈值。 */ + max?: number; + /** 采样数量,越大轮廓线越接近真实概率分布函数,默认32。 */ + sampleSize?: number; + /** 核函数的带宽。带宽越大产生的曲线越平滑(越模糊),带宽越小产生的曲线越陡峭。默认3。 */ + width?: number; +}; +``` + +#### meta + +`markdown:docs/common/meta.zh.md` + +### 图形样式 + +#### shape + +**optional** _'smooth'|'hollow'|'hollow-smooth'_ + +小提琴形状。 +* 默认: 非平滑、实心 +* smooth: 平滑 +* hollow: 空心 +* hollow-smooth: 平滑、空心 + +#### violinStyle + +**optional** _StyleAttr | Function_ + +小提琴轮廓样式配置。 + +`markdown:docs/common/shape-style.zh.md` + +### box + +**optional** _false | object_ + +小提琴图内置的箱线图配置。设置为 `false` 时不渲染箱线图。 + +箱线图的统计数据分别为: + +- max: 数据中的最大值,作为箱线图的最高点; +- min: 数据中的最小值,作为箱线图的最低点; +- q3: 上四分位,即 25% 的数据大于该数,作为箱线图中箱子的高点; +- q1: 下四分位,即 25% 的数据小于该数,作为箱线图中箱子的低点; +- median: 数据的中位数,在箱线图中用圆点表示。 + +可以在 `box.textMap` 中指定文案。 + +```ts +type BoxOptions = false | { + /** 箱线图的文案映射 */ + textMap?: { + /** 最大值文案 */ + max: string; + /** 最小值文案 */ + min: string; + /** 下四分位数文案 */ + q1: string; + /** 上四分位数文案 */ + q3: string; + /** 中位数文案 */ + median: string; + }; +}; +``` + +`markdown:docs/common/color.zh.md` + +### 图表组件 + +`markdown:docs/common/component.zh.md` + +### 图表事件 + +`markdown:docs/common/events.zh.md` + +### 图表方法 + +`markdown:docs/common/chart-methods.zh.md` + +### 图表主题 + +`markdown:docs/common/theme.zh.md` diff --git a/examples/more-plots/violin/API.en.md b/examples/more-plots/violin/API.en.md new file mode 100644 index 0000000000..c2fe954f72 --- /dev/null +++ b/examples/more-plots/violin/API.en.md @@ -0,0 +1 @@ +`markdown:docs/api/plots/violin.en.md` \ No newline at end of file diff --git a/examples/more-plots/violin/API.zh.md b/examples/more-plots/violin/API.zh.md new file mode 100644 index 0000000000..b2f7dec1f1 --- /dev/null +++ b/examples/more-plots/violin/API.zh.md @@ -0,0 +1 @@ +`markdown:docs/api/plots/violin.zh.md` \ No newline at end of file diff --git a/examples/more-plots/violin/demo/basic.ts b/examples/more-plots/violin/demo/basic.ts new file mode 100644 index 0000000000..96e34cf815 --- /dev/null +++ b/examples/more-plots/violin/demo/basic.ts @@ -0,0 +1,14 @@ +import { Violin } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/bmw-prod/6b0a5f1d-5931-42ae-b3ba-3c3cb77d0861.json') + .then((response) => response.json()) + .then((data) => { + const violinPlot = new Violin('container', { + width: 400, + height: 500, + data: data, + xField: 'x', + yField: 'y', + }); + violinPlot.render(); + }); diff --git a/examples/more-plots/violin/demo/group.ts b/examples/more-plots/violin/demo/group.ts new file mode 100644 index 0000000000..740d063b6f --- /dev/null +++ b/examples/more-plots/violin/demo/group.ts @@ -0,0 +1,15 @@ +import { Violin } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/bmw-prod/6b0a5f1d-5931-42ae-b3ba-3c3cb77d0861.json') + .then((response) => response.json()) + .then((data) => { + const violinPlot = new Violin('container', { + width: 400, + height: 500, + data: data, + xField: 'x', + yField: 'y', + seriesField: 'species', + }); + violinPlot.render(); + }); diff --git a/examples/more-plots/violin/demo/meta.json b/examples/more-plots/violin/demo/meta.json new file mode 100644 index 0000000000..1bb8b55886 --- /dev/null +++ b/examples/more-plots/violin/demo/meta.json @@ -0,0 +1,40 @@ +{ + "title": { + "zh": "小提琴图", + "en": "Violin" + }, + "demos": [ + { + "filename": "basic.ts", + "title": { + "zh": "基础小提琴图", + "en": "Basic violin plot" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*mBJ1Rqh72ScAAAAAAAAAAAAAARQnAQ" + }, + { + "filename": "group.ts", + "title": { + "zh": "分组小提琴图", + "en": "Grouped violin plot" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*eb3AQ6CiBLwAAAAAAAAAAAAAARQnAQ" + }, + { + "filename": "shape.ts", + "title": { + "zh": "平滑/空心小提琴图", + "en": "Smooth/Hollow violin plot" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*0nOgSohp4QAAAAAAAAAAAAAAARQnAQ" + }, + { + "filename": "tooltip.ts", + "title": { + "zh": "自定义Tooltip文案", + "en": "Customize tooltip texts" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*IsxHQYqkeEsAAAAAAAAAAAAAARQnAQ" + } + ] +} \ No newline at end of file diff --git a/examples/more-plots/violin/demo/shape.ts b/examples/more-plots/violin/demo/shape.ts new file mode 100644 index 0000000000..d115d4acc9 --- /dev/null +++ b/examples/more-plots/violin/demo/shape.ts @@ -0,0 +1,15 @@ +import { Violin } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/bmw-prod/6b0a5f1d-5931-42ae-b3ba-3c3cb77d0861.json') + .then((response) => response.json()) + .then((data) => { + const violinPlot = new Violin('container', { + width: 400, + height: 500, + data: data, + xField: 'x', + yField: 'y', + shape: 'hollow-smooth', // or 'hollow', 'smooth' + }); + violinPlot.render(); + }); diff --git a/examples/more-plots/violin/demo/tooltip.ts b/examples/more-plots/violin/demo/tooltip.ts new file mode 100644 index 0000000000..db16972dcc --- /dev/null +++ b/examples/more-plots/violin/demo/tooltip.ts @@ -0,0 +1,36 @@ +import { Violin } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/bmw-prod/6b0a5f1d-5931-42ae-b3ba-3c3cb77d0861.json') + .then((response) => response.json()) + .then((data) => { + const violinPlot = new Violin('container', { + width: 400, + height: 500, + data: data, + xField: 'x', + yField: 'y', + box: { + textMap: { + max: '最大值', + min: '最小值', + q3: '上四分位', + q1: '下四分位', + median: '中位数', + }, + }, + tooltip: { + formatter: (datum) => { + return { + value: { + max: datum.minMax[0] + '%', + min: datum.minMax[1] + '%', + q1: datum.quantile[0] + '%', + q3: datum.quantile[1] + '%', + median: datum.median[0] + '%', + }, + }; + }, + }, + }); + violinPlot.render(); + }); diff --git a/examples/more-plots/violin/index.en.md b/examples/more-plots/violin/index.en.md new file mode 100644 index 0000000000..2dfefebaf0 --- /dev/null +++ b/examples/more-plots/violin/index.en.md @@ -0,0 +1,4 @@ +--- +title: Violin +order: 11 +--- diff --git a/examples/more-plots/violin/index.zh.md b/examples/more-plots/violin/index.zh.md new file mode 100644 index 0000000000..ab69780e9b --- /dev/null +++ b/examples/more-plots/violin/index.zh.md @@ -0,0 +1,4 @@ +--- +title: 小提琴图 +order: 11 +--- diff --git a/package.json b/package.json index 719e1e92ab..e8a9241fec 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@antv/g2": "^4.1.0", "d3-hierarchy": "^2.0.0", "d3-regression": "^1.3.5", + "pdfast": "^0.2.0", "size-sensor": "^1.0.1", "tslib": "^2.0.3" }, @@ -88,6 +89,7 @@ "jest": "^26.0.1", "jest-electron": "^0.1.7", "jest-extended": "^0.11.2", + "jest-matcher-deep-close-to": "^2.0.1", "limit-size": "^0.1.3", "lint-md-cli": "^0.1.2", "lint-staged": "^10.0.7", diff --git a/src/adaptor/geometries/index.ts b/src/adaptor/geometries/index.ts index 35201828de..1cc9132da6 100644 --- a/src/adaptor/geometries/index.ts +++ b/src/adaptor/geometries/index.ts @@ -12,3 +12,5 @@ export { edge } from './edge'; export type { EdgeGeometryOptions } from './edge'; export { schema } from './schema'; export type { SchemaGeometryOptions } from './schema'; +export { violin } from './violin'; +export type { ViolinGeometryOptions } from './violin'; diff --git a/src/adaptor/geometries/violin.ts b/src/adaptor/geometries/violin.ts new file mode 100644 index 0000000000..5476ad7610 --- /dev/null +++ b/src/adaptor/geometries/violin.ts @@ -0,0 +1,44 @@ +import { Params } from '../../core/adaptor'; +import { getTooltipMapping } from '../../utils/tooltip'; +import { deepAssign } from '../../utils'; +import { geometry, MappingOptions, GeometryOptions } from './base'; + +export interface ViolinGeometryOptions extends GeometryOptions { + /** x 轴字段 */ + readonly xField?: string; + /** y 轴字段(指小提琴的Y轴,即概率密度) */ + readonly yField?: string; + /** 分组字段 */ + readonly seriesField?: string; + /** size 映射字段 */ + readonly sizeField?: string; + /** violin 图形映射规则 */ + readonly violin?: MappingOptions; +} + +/** + * violin 辅助点的配置处理 + * @param params + */ +export function violin(params: Params): Params { + const { options } = params; + const { violin, xField, yField, seriesField, sizeField, tooltip } = options; + + const { fields, formatter } = getTooltipMapping(tooltip, [xField, yField, seriesField, sizeField]); + + return violin + ? geometry( + deepAssign({}, params, { + options: { + type: 'violin', + colorField: seriesField, + tooltipFields: fields, + mapping: { + tooltip: formatter, + ...violin, + }, + }, + }) + ) + : params; +} diff --git a/src/index.ts b/src/index.ts index 8dda129d9d..7752203247 100644 --- a/src/index.ts +++ b/src/index.ts @@ -85,6 +85,10 @@ export type { HeatmapOptions } from './plots/heatmap'; export { Box } from './plots/box'; export type { BoxOptions } from './plots/box'; +// 小提琴图及类型定义 +export { Violin } from './plots/violin'; +export type { ViolinOptions } from './plots/violin'; + // K线图及类型定义 | author by [jhwong](https://github.com/jinhuiWong) export { Stock } from './plots/stock'; export type { StockOptions } from './plots/stock'; diff --git a/src/plots/violin/adaptor.ts b/src/plots/violin/adaptor.ts new file mode 100644 index 0000000000..fdce10ec6d --- /dev/null +++ b/src/plots/violin/adaptor.ts @@ -0,0 +1,315 @@ +import { Params } from '../../core/adaptor'; +import { interaction, animation, theme } from '../../adaptor/common'; +import { interval, point, violin } from '../../adaptor/geometries'; +import { flow, pick, deepAssign } from '../../utils'; +import { AXIS_META_CONFIG_KEYS } from '../../constant'; +import { Datum } from '../../types'; +import { ViolinOptions } from './types'; +import { transformViolinData } from './utils'; +import { + MEDIAN_FIELD, + MEDIAN_VIEW_ID, + MIN_MAX_FIELD, + MIN_MAX_VIEW_ID, + QUANTILE_FIELD, + QUANTILE_VIEW_ID, + SERIES_FIELD, + VIOLIN_SIZE_FIELD, + VIOLIN_VIEW_ID, + VIOLIN_Y_FIELD, + X_FIELD, +} from './constant'; + +const ALL_FIELDS = [ + X_FIELD, + SERIES_FIELD, + VIOLIN_Y_FIELD, + VIOLIN_SIZE_FIELD, + MIN_MAX_FIELD, + QUANTILE_FIELD, + MEDIAN_FIELD, +]; + +const adjustCfg = [ + { + type: 'dodge', + marginRatio: 1 / 32, + } as const, +]; + +/** 处理数据 */ +function data(params: Params): Params { + const { chart, options } = params; + chart.data(transformViolinData(options)); + return params; +} + +/** 小提琴轮廓 */ +function violinView(params: Params): Params { + const { chart, options } = params; + const { seriesField, color, shape = 'violin', violinStyle, tooltip } = options; + + const view = chart.createView({ id: VIOLIN_VIEW_ID }); + violin({ + chart: view, + options: { + xField: X_FIELD, + yField: VIOLIN_Y_FIELD, + seriesField: seriesField ? SERIES_FIELD : X_FIELD, + sizeField: VIOLIN_SIZE_FIELD, + tooltip: { + fields: ALL_FIELDS, + ...tooltip, + }, + violin: { + style: violinStyle, + color, + shape, + }, + }, + }); + view.geometries[0].adjust(adjustCfg); + + view.axis(VIOLIN_Y_FIELD, { + grid: { + line: null, + }, + tickLine: { + alignTick: false, + }, + }); + view.axis(VIOLIN_Y_FIELD, { + grid: { + line: { + style: { + lineWidth: 0.5, + // TODO: 为什么是 dash ? + lineDash: [4, 4], + }, + }, + }, + }); + + return params; +} + +/** 箱线 */ +function boxView(params: Params): Params { + const { chart, options } = params; + const { seriesField, color, box, tooltip } = options; + + // 如果配置 `box` 为 false ,不渲染内部箱线图 + if (!box) return params; + + // 边缘线 + const minMaxView = chart.createView({ id: MIN_MAX_VIEW_ID }); + interval({ + chart: minMaxView, + options: { + xField: X_FIELD, + yField: MIN_MAX_FIELD, + seriesField: seriesField ? SERIES_FIELD : X_FIELD, + tooltip: { + fields: ALL_FIELDS, + ...tooltip, + }, + interval: { + color, + size: 1, + style: { + lineWidth: 0, + }, + }, + }, + }); + minMaxView.geometries[0].adjust(adjustCfg); + + // 四分点位 + const quantileView = chart.createView({ id: QUANTILE_VIEW_ID }); + interval({ + chart: quantileView, + options: { + xField: X_FIELD, + yField: QUANTILE_FIELD, + seriesField: seriesField ? SERIES_FIELD : X_FIELD, + tooltip: { + fields: ALL_FIELDS, + ...tooltip, + }, + interval: { + color, + size: 8, + style: { + fillOpacity: 1, + }, + }, + }, + }); + quantileView.geometries[0].adjust(adjustCfg); + + // 中位值 + const medianView = chart.createView({ id: MEDIAN_VIEW_ID }); + point({ + chart: medianView, + options: { + xField: X_FIELD, + yField: MEDIAN_FIELD, + seriesField: seriesField ? SERIES_FIELD : X_FIELD, + tooltip: { + fields: ALL_FIELDS, + ...tooltip, + }, + point: { + color, + size: 1, + style: { + fill: 'white', + lineWidth: 0, + }, + }, + }, + }); + medianView.geometries[0].adjust(adjustCfg); + + return params; +} + +/** + * meta 配置 + */ +function meta(params: Params): Params { + const { chart, options } = params; + const { meta, xAxis, yAxis } = options; + + const baseMeta = {}; + + const scales = deepAssign(baseMeta, meta, { + [X_FIELD]: { + sync: true, + ...pick(xAxis, AXIS_META_CONFIG_KEYS), + }, + [VIOLIN_Y_FIELD]: { + sync: 'y', + ...pick(yAxis, AXIS_META_CONFIG_KEYS), + }, + [MIN_MAX_FIELD]: { + sync: 'y', + ...pick(yAxis, AXIS_META_CONFIG_KEYS), + }, + [QUANTILE_FIELD]: { + sync: 'y', + ...pick(yAxis, AXIS_META_CONFIG_KEYS), + }, + [MEDIAN_FIELD]: { + sync: 'y', + ...pick(yAxis, AXIS_META_CONFIG_KEYS), + }, + }); + + chart.scale(scales); + + return params; +} + +/** + * axis 配置 + */ +function axis(params: Params): Params { + const { chart, options } = params; + const { xAxis, yAxis, xField, yField, tooltip } = options; + + // 为 false 则是不显示轴 + if (xAxis === false) { + chart.axis(xField, false); + } else { + chart.axis(xField, xAxis); + } + + chart.axis(yField, yAxis); + + return params; +} + +function tooltip(params: Params): Params { + const { chart, options } = params; + const { box } = options; + if (!box) return params; + + const textMap = box.textMap; + chart.tooltip( + deepAssign( + {}, + // 内置的配置 + { + showMarkers: false, + // 默认 formatter 把 datum 转换为 { value: { min, max, q1, q3, median } } 的结构体。 + formatter: (datum: Datum) => { + return { + value: { + min: datum.minMax[0], + max: datum.minMax[1], + q1: datum.quantile[0], + q3: datum.quantile[1], + median: datum.median[0], + }, + }; + }, + // 默认 customItems 消费上述结构体。 + customItems: (originalItems) => { + const sample = originalItems?.[0]; + if (!sample) return []; + + return [ + { + ...sample, + name: textMap.max, + title: textMap.max, + value: sample.value.max, + marker: 'circle', + }, + { + ...sample, + name: textMap.q3, + title: textMap.q3, + value: sample.value.q3, + marker: 'circle', + }, + { + ...sample, + name: textMap.median, + title: textMap.median, + value: sample.value.median, + marker: 'circle', + }, + { + ...sample, + name: textMap.q1, + title: textMap.q1, + value: sample.value.q1, + marker: 'circle', + }, + { + ...sample, + name: textMap.min, + title: textMap.min, + value: sample.value.min, + marker: 'circle', + }, + ]; + }, + }, + // 用户的配置 + options.tooltip + ) + ); + + return params; +} + +/** + * 箱型图适配器 + * @param params + */ +export function adaptor(params: Params) { + return flow(data, violinView, boxView, meta, tooltip, axis, interaction, animation, theme)(params); +} diff --git a/src/plots/violin/constant.ts b/src/plots/violin/constant.ts new file mode 100644 index 0000000000..f844c01834 --- /dev/null +++ b/src/plots/violin/constant.ts @@ -0,0 +1,49 @@ +import { Plot } from '../../core/plot'; +import { deepAssign } from '../../utils'; +import { ViolinOptions } from './types'; + +export const X_FIELD = 'x'; +export const SERIES_FIELD = 'series'; +export const VIOLIN_Y_FIELD = 'violinY'; +export const VIOLIN_SIZE_FIELD = 'violinSize'; +export const MIN_MAX_FIELD = 'minMax'; +export const QUANTILE_FIELD = 'quantile'; +export const MEDIAN_FIELD = 'median'; + +export const VIOLIN_VIEW_ID = 'violin_view'; +export const MIN_MAX_VIEW_ID = 'min_max_view'; +export const QUANTILE_VIEW_ID = 'quantile_view'; +export const MEDIAN_VIEW_ID = 'median_view'; + +export const DEFAULT_OPTIONS = deepAssign({}, Plot.getDefaultOptions(), { + // 默认核函数 + kde: { + type: 'triangular', + sampleSize: 32, + width: 3, + }, + + // 默认平滑 + smooth: true, + + // 默认显示箱线图 + box: { + textMap: { + max: 'Max', + min: 'Min', + q1: 'Q1', + q3: 'Q3', + median: 'Median', + }, + }, + + // 默认小提琴轮廓样式 + violinStyle: { + lineWidth: 1, + fillOpacity: 0.3, + strokeOpacity: 0.75, + }, + + // 默认区域交互 + // interactions: [{ type: 'active-region' }], +} as Partial); diff --git a/src/plots/violin/index.ts b/src/plots/violin/index.ts new file mode 100644 index 0000000000..53dfe1bc93 --- /dev/null +++ b/src/plots/violin/index.ts @@ -0,0 +1,42 @@ +import { Plot } from '../../core/plot'; +import { Adaptor } from '../../core/adaptor'; +import { ViolinOptions } from './types'; +import { adaptor } from './adaptor'; +import { DEFAULT_OPTIONS } from './constant'; +import { transformViolinData } from './utils'; +export type { ViolinOptions }; + +export class Violin extends Plot { + /** + * 获取 默认配置项 + * 供外部使用 + */ + static getDefaultOptions(): Partial { + return DEFAULT_OPTIONS; + } + + /** 图表类型 */ + public type: string = 'violin'; + + /** + * @override + */ + public changeData(data: ViolinOptions['data']) { + this.updateOption({ data }); + this.chart.changeData(transformViolinData(this.options)); + } + + /** + * 获取 小提琴图 默认配置项 + */ + protected getDefaultOptions(): Partial { + return Violin.getDefaultOptions(); + } + + /** + * 获取 小提琴图 的适配器 + */ + protected getSchemaAdaptor(): Adaptor { + return adaptor; + } +} diff --git a/src/plots/violin/types.ts b/src/plots/violin/types.ts new file mode 100644 index 0000000000..9cbb65825f --- /dev/null +++ b/src/plots/violin/types.ts @@ -0,0 +1,51 @@ +import { Options, StyleAttr } from '../../types'; + +export interface ViolinOptions extends Options { + /** X 轴映射 */ + readonly xField: string; + /** Y 轴映射 */ + readonly yField: string; + /** 拆分字段映射,默认是分组情况,颜色作为视觉通道 */ + readonly seriesField?: string; + /** + * 小提琴的形状。 + * 默认: 非平滑、实心 + * smooth: 平滑 + * hollow: 空心 + * hollow-smooth: 平滑、空心 + */ + readonly shape?: 'smooth' | 'hollow' | 'hollow-smooth'; + /** 小提琴样式配置,可选 */ + readonly violinStyle?: StyleAttr; + /** 核函数配置,当前只支持三角核 */ + readonly kde?: { + /** 三角波类型 */ + type: 'triangular'; + /** 最小值,默认为数据中的最小值减去一个固定的阈值。 */ + min?: number; + /** 最大值,默认为数据中的最大值加上一个固定的阈值。 */ + max?: number; + /** 采样数量,越大轮廓线越接近真实概率分布函数,默认32。 */ + sampleSize?: number; + /** 核函数的带宽。带宽越大产生的曲线越平滑(越模糊),带宽越小产生的曲线越陡峭。默认3。 */ + width?: number; + } /* | { type: 'gaussian', } ⬅️ 像这样添加新的核函数支持 */; + /** 内部箱线图配置,false 为不显示。 */ + readonly box?: + | false + | { + /** 箱线图的文案映射 */ + textMap?: { + /** 最大值文案 */ + max: string; + /** 最小值文案 */ + min: string; + /** 下四分位数文案 */ + q1: string; + /** 上四分位数文案 */ + q3: string; + /** 中位数文案 */ + median: string; + }; + }; +} diff --git a/src/plots/violin/utils.ts b/src/plots/violin/utils.ts new file mode 100644 index 0000000000..4c56728be0 --- /dev/null +++ b/src/plots/violin/utils.ts @@ -0,0 +1,90 @@ +import { groupBy, min, max } from '@antv/util'; +import pdf from 'pdfast'; +import { quantile } from '../../utils/transform/quantile'; +import { ViolinOptions } from './types'; + +export type ViolinData = { + /** X轴 */ + x: string; + /** 拆分 */ + series?: string; + + /** 小提琴轮廓的 size 通道数据 */ + violinSize: number[]; + /** 小提琴轮廓的 y 通道数据 */ + violinY: number[]; + + /** 箱线图中的上线边缘线 */ + minMax: number[]; + /** 箱线图中的上下四分位点 */ + quantile: number[]; + /** 箱线图中的中位值 */ + median: number[]; +}; + +export type PdfOptions = { + min: number; + max: number; + size: number; + width: number; +}; + +export const toBoxValue = (values: number[]) => { + return { + minMax: [min(values), max(values)], + quantile: [quantile(values, 0.25), quantile(values, 0.75)], + median: quantile(values, [0.5]), + }; +}; + +export const toViolinValue = (values: number[], pdfOptions: PdfOptions) => { + const pdfResults: Array<{ x: number; y: number }> = pdf.create(values, pdfOptions); + return { + violinSize: pdfResults.map((result) => result.y), + violinY: pdfResults.map((result) => result.x), + }; +}; + +export const transformViolinData = (options: ViolinOptions): ViolinData[] => { + const { xField, yField, seriesField, data, kde } = options; + + /** 生成概率密度函数的配置 */ + const pdfOptions: PdfOptions = { + min: kde.min, + max: kde.max, + size: kde.sampleSize, + width: kde.width, + }; + + // 无拆分 + if (!seriesField) { + const group = groupBy(data, xField); + return Object.keys(group).map((x) => { + const records = group[x]; + const values = records.map((record) => record[yField]); + return { + x, + ...toViolinValue(values, pdfOptions), + ...toBoxValue(values), + }; + }); + } + + // 有拆分 + const resultList: ViolinData[] = []; + const seriesGroup = groupBy(data, seriesField); + Object.keys(seriesGroup).forEach((series) => { + const group = groupBy(seriesGroup[series], xField); + return Object.keys(group).forEach((key) => { + const records = group[key]; + const values = records.map((record) => record[yField]); + resultList.push({ + x: key, + series, + ...toViolinValue(values, pdfOptions), + ...toBoxValue(values), + }); + }); + }); + return resultList; +}; diff --git a/src/utils/transform/quantile.ts b/src/utils/transform/quantile.ts new file mode 100644 index 0000000000..a99d822e98 --- /dev/null +++ b/src/utils/transform/quantile.ts @@ -0,0 +1,210 @@ +// from https://github.com/simple-statistics + +/** + * This is the internal implementation of quantiles: when you know + * that the order is sorted, you don't need to re-sort it, and the computations + * are faster. + * + * @param {Array} x sample of one or more data points + * @param {number} p desired quantile: a number between 0 to 1, inclusive + * @returns {number} quantile value + * @throws {Error} if p ix outside of the range from 0 to 1 + * @throws {Error} if x is empty + * @example + * quantileSorted([3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 0.5); // => 9 + */ +function quantileSorted(x, p) { + const idx = x.length * p; + if (x.length === 0) { + throw new Error('quantile requires at least one data point.'); + } else if (p < 0 || p > 1) { + throw new Error('quantiles must be between 0 and 1'); + } else if (p === 1) { + // If p is 1, directly return the last element + return x[x.length - 1]; + } else if (p === 0) { + // If p is 0, directly return the first element + return x[0]; + } else if (idx % 1 !== 0) { + // If p is not integer, return the next element in array + return x[Math.ceil(idx) - 1]; + } else if (x.length % 2 === 0) { + // If the list has even-length, we'll take the average of this number + // and the next value, if there is one + return (x[idx - 1] + x[idx]) / 2; + } else { + // Finally, in the simple case of an integer value + // with an odd-length list, return the x value at the index. + return x[idx]; + } +} + +function swap(arr, i, j) { + const tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; +} + +/** + * Rearrange items in `arr` so that all items in `[left, k]` range are the smallest. + * The `k`-th element will have the `(k - left + 1)`-th smallest value in `[left, right]`. + * + * Implements Floyd-Rivest selection algorithm https://en.wikipedia.org/wiki/Floyd-Rivest_algorithm + * + * @param {Array} arr input array + * @param {number} k pivot index + * @param {number} [left] left index + * @param {number} [right] right index + * @returns {void} mutates input array + * @example + * var arr = [65, 28, 59, 33, 21, 56, 22, 95, 50, 12, 90, 53, 28, 77, 39]; + * quickselect(arr, 8); + * // = [39, 28, 28, 33, 21, 12, 22, 50, 53, 56, 59, 65, 90, 77, 95] + */ +function quickselect(arr, k, left, right) { + left = left || 0; + right = right || arr.length - 1; + + while (right > left) { + // 600 and 0.5 are arbitrary constants chosen in the original paper to minimize execution time + if (right - left > 600) { + const n = right - left + 1; + const m = k - left + 1; + const z = Math.log(n); + const s = 0.5 * Math.exp((2 * z) / 3); + let sd = 0.5 * Math.sqrt((z * s * (n - s)) / n); + if (m - n / 2 < 0) sd *= -1; + const newLeft = Math.max(left, Math.floor(k - (m * s) / n + sd)); + const newRight = Math.min(right, Math.floor(k + ((n - m) * s) / n + sd)); + quickselect(arr, k, newLeft, newRight); + } + + const t = arr[k]; + let i = left; + let j = right; + + swap(arr, left, k); + if (arr[right] > t) swap(arr, left, right); + + while (i < j) { + swap(arr, i, j); + i++; + j--; + while (arr[i] < t) i++; + while (arr[j] > t) j--; + } + + if (arr[left] === t) swap(arr, left, j); + else { + j++; + swap(arr, j, right); + } + + if (j <= k) left = j + 1; + if (k <= j) right = j - 1; + } +} + +/** + * The [quantile](https://en.wikipedia.org/wiki/Quantile): + * this is a population quantile, since we assume to know the entire + * dataset in this library. This is an implementation of the + * [Quantiles of a Population](http://en.wikipedia.org/wiki/Quantile#Quantiles_of_a_population) + * algorithm from wikipedia. + * + * Sample is a one-dimensional array of numbers, + * and p is either a decimal number from 0 to 1 or an array of decimal + * numbers from 0 to 1. + * In terms of a k/q quantile, p = k/q - it's just dealing with fractions or dealing + * with decimal values. + * When p is an array, the result of the function is also an array containing the appropriate + * quantiles in input order + * + * @param {Array} x sample of one or more numbers + * @param {Array | number} p the desired quantile, as a number between 0 and 1 + * @returns {number} quantile + * @example + * quantile([3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 0.5); // => 9 + */ +function quantile(x: number[], p: number): number; +function quantile(x: number[], p: number[]): number[]; +function quantile(x: any, p: any): any { + const copy = x.slice(); + + if (Array.isArray(p)) { + // rearrange elements so that each element corresponding to a requested + // quantile is on a place it would be if the array was fully sorted + multiQuantileSelect(copy, p); + // Initialize the result array + const results: number[] = []; + // For each requested quantile + for (let i = 0; i < p.length; i++) { + results[i] = quantileSorted(copy, p[i]); + } + return results; + } else { + const idx = quantileIndex(copy.length, p); + quantileSelect(copy, idx, 0, copy.length - 1); + return quantileSorted(copy, p); + } +} + +function quantileSelect(arr, k, left, right) { + if (k % 1 === 0) { + quickselect(arr, k, left, right); + } else { + k = Math.floor(k); + quickselect(arr, k, left, right); + quickselect(arr, k + 1, k + 1, right); + } +} + +function multiQuantileSelect(arr, p) { + const indices = [0]; + for (let i = 0; i < p.length; i++) { + indices.push(quantileIndex(arr.length, p[i])); + } + indices.push(arr.length - 1); + indices.sort(compare); + + const stack = [0, indices.length - 1]; + + while (stack.length) { + const r = Math.ceil(stack.pop()); + const l = Math.floor(stack.pop()); + if (r - l <= 1) continue; + + const m = Math.floor((l + r) / 2); + quantileSelect(arr, indices[m], Math.floor(indices[l]), Math.ceil(indices[r])); + + stack.push(l, m, m, r); + } +} + +function compare(a, b) { + return a - b; +} + +function quantileIndex(len, p) { + const idx = len * p; + if (p === 1) { + // If p is 1, directly return the last index + return len - 1; + } else if (p === 0) { + // If p is 0, directly return the first index + return 0; + } else if (idx % 1 !== 0) { + // If index is not integer, return the next index in array + return Math.ceil(idx) - 1; + } else if (len % 2 === 0) { + // If the list has even-length, we'll return the middle of two indices + // around quantile to indicate that we need an average value of the two + return idx - 0.5; + } else { + // Finally, in the simple case of an integer index + // with an odd-length list, return the index + return idx; + } +} + +export { quantile }; From 802f994554bc1deb90b4eb957b7dbf6794a59172 Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Sat, 29 May 2021 11:54:36 +0800 Subject: [PATCH 02/18] =?UTF-8?q?fix(violin):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=B0=8F=E6=8F=90=E7=90=B4=E5=9B=BE=E8=BD=B4=E9=87=8D=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plots/violin/adaptor.ts | 18 ++++++++++++++---- src/plots/violin/constant.ts | 2 ++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/plots/violin/adaptor.ts b/src/plots/violin/adaptor.ts index fdce10ec6d..38c323923e 100644 --- a/src/plots/violin/adaptor.ts +++ b/src/plots/violin/adaptor.ts @@ -171,6 +171,16 @@ function boxView(params: Params): Params { }); medianView.geometries[0].adjust(adjustCfg); + // 关闭辅助 view 的轴 + quantileView.axis(false); + minMaxView.axis(false); + medianView.axis(false); + + // 关闭辅助 view 的图例 + medianView.legend(false); + minMaxView.legend(false); + quantileView.legend(false); + return params; } @@ -189,19 +199,19 @@ function meta(params: Params): Params { ...pick(xAxis, AXIS_META_CONFIG_KEYS), }, [VIOLIN_Y_FIELD]: { - sync: 'y', + sync: true, ...pick(yAxis, AXIS_META_CONFIG_KEYS), }, [MIN_MAX_FIELD]: { - sync: 'y', + sync: VIOLIN_Y_FIELD, ...pick(yAxis, AXIS_META_CONFIG_KEYS), }, [QUANTILE_FIELD]: { - sync: 'y', + sync: VIOLIN_Y_FIELD, ...pick(yAxis, AXIS_META_CONFIG_KEYS), }, [MEDIAN_FIELD]: { - sync: 'y', + sync: VIOLIN_Y_FIELD, ...pick(yAxis, AXIS_META_CONFIG_KEYS), }, }); diff --git a/src/plots/violin/constant.ts b/src/plots/violin/constant.ts index f844c01834..97f54561d4 100644 --- a/src/plots/violin/constant.ts +++ b/src/plots/violin/constant.ts @@ -16,6 +16,8 @@ export const QUANTILE_VIEW_ID = 'quantile_view'; export const MEDIAN_VIEW_ID = 'median_view'; export const DEFAULT_OPTIONS = deepAssign({}, Plot.getDefaultOptions(), { + // 多 view 组成,一定要设置 view padding 同步 + syncViewPadding: true, // 默认核函数 kde: { type: 'triangular', From 811f9829fe08604b2f78dc6d494eaeb887df510b Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Sat, 29 May 2021 12:25:35 +0800 Subject: [PATCH 03/18] =?UTF-8?q?fix(violin):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=B0=8F=E6=8F=90=E7=90=B4=E5=9B=BE=20axis,=20legend=20?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/more-plots/violin/demo/basic.ts | 1 - examples/more-plots/violin/demo/group.ts | 1 - examples/more-plots/violin/demo/meta.json | 12 ++-- examples/more-plots/violin/demo/shape.ts | 1 - examples/more-plots/violin/demo/tooltip.ts | 1 - src/plots/violin/adaptor.ts | 64 +++++++++++++--------- src/plots/violin/constant.ts | 22 ++++++++ 7 files changed, 68 insertions(+), 34 deletions(-) diff --git a/examples/more-plots/violin/demo/basic.ts b/examples/more-plots/violin/demo/basic.ts index 96e34cf815..c702f84866 100644 --- a/examples/more-plots/violin/demo/basic.ts +++ b/examples/more-plots/violin/demo/basic.ts @@ -4,7 +4,6 @@ fetch('https://gw.alipayobjects.com/os/bmw-prod/6b0a5f1d-5931-42ae-b3ba-3c3cb77d .then((response) => response.json()) .then((data) => { const violinPlot = new Violin('container', { - width: 400, height: 500, data: data, xField: 'x', diff --git a/examples/more-plots/violin/demo/group.ts b/examples/more-plots/violin/demo/group.ts index 740d063b6f..8bdba98026 100644 --- a/examples/more-plots/violin/demo/group.ts +++ b/examples/more-plots/violin/demo/group.ts @@ -4,7 +4,6 @@ fetch('https://gw.alipayobjects.com/os/bmw-prod/6b0a5f1d-5931-42ae-b3ba-3c3cb77d .then((response) => response.json()) .then((data) => { const violinPlot = new Violin('container', { - width: 400, height: 500, data: data, xField: 'x', diff --git a/examples/more-plots/violin/demo/meta.json b/examples/more-plots/violin/demo/meta.json index 1bb8b55886..63035c0d19 100644 --- a/examples/more-plots/violin/demo/meta.json +++ b/examples/more-plots/violin/demo/meta.json @@ -10,7 +10,8 @@ "zh": "基础小提琴图", "en": "Basic violin plot" }, - "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*mBJ1Rqh72ScAAAAAAAAAAAAAARQnAQ" + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*mBJ1Rqh72ScAAAAAAAAAAAAAARQnAQ", + "new": true }, { "filename": "group.ts", @@ -18,7 +19,8 @@ "zh": "分组小提琴图", "en": "Grouped violin plot" }, - "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*eb3AQ6CiBLwAAAAAAAAAAAAAARQnAQ" + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*eb3AQ6CiBLwAAAAAAAAAAAAAARQnAQ", + "new": true }, { "filename": "shape.ts", @@ -26,7 +28,8 @@ "zh": "平滑/空心小提琴图", "en": "Smooth/Hollow violin plot" }, - "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*0nOgSohp4QAAAAAAAAAAAAAAARQnAQ" + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*0nOgSohp4QAAAAAAAAAAAAAAARQnAQ", + "new": true }, { "filename": "tooltip.ts", @@ -34,7 +37,8 @@ "zh": "自定义Tooltip文案", "en": "Customize tooltip texts" }, - "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*IsxHQYqkeEsAAAAAAAAAAAAAARQnAQ" + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*IsxHQYqkeEsAAAAAAAAAAAAAARQnAQ", + "new": true } ] } \ No newline at end of file diff --git a/examples/more-plots/violin/demo/shape.ts b/examples/more-plots/violin/demo/shape.ts index d115d4acc9..d7ef6777d5 100644 --- a/examples/more-plots/violin/demo/shape.ts +++ b/examples/more-plots/violin/demo/shape.ts @@ -4,7 +4,6 @@ fetch('https://gw.alipayobjects.com/os/bmw-prod/6b0a5f1d-5931-42ae-b3ba-3c3cb77d .then((response) => response.json()) .then((data) => { const violinPlot = new Violin('container', { - width: 400, height: 500, data: data, xField: 'x', diff --git a/examples/more-plots/violin/demo/tooltip.ts b/examples/more-plots/violin/demo/tooltip.ts index db16972dcc..bc29924628 100644 --- a/examples/more-plots/violin/demo/tooltip.ts +++ b/examples/more-plots/violin/demo/tooltip.ts @@ -4,7 +4,6 @@ fetch('https://gw.alipayobjects.com/os/bmw-prod/6b0a5f1d-5931-42ae-b3ba-3c3cb77d .then((response) => response.json()) .then((data) => { const violinPlot = new Violin('container', { - width: 400, height: 500, data: data, xField: 'x', diff --git a/src/plots/violin/adaptor.ts b/src/plots/violin/adaptor.ts index 38c323923e..aa2e05d934 100644 --- a/src/plots/violin/adaptor.ts +++ b/src/plots/violin/adaptor.ts @@ -1,7 +1,8 @@ +import { get, omit, each } from '@antv/util'; import { Params } from '../../core/adaptor'; import { interaction, animation, theme } from '../../adaptor/common'; import { interval, point, violin } from '../../adaptor/geometries'; -import { flow, pick, deepAssign } from '../../utils'; +import { flow, pick, deepAssign, findViewById } from '../../utils'; import { AXIS_META_CONFIG_KEYS } from '../../constant'; import { Datum } from '../../types'; import { ViolinOptions } from './types'; @@ -70,26 +71,6 @@ function violinView(params: Params): Params { }); view.geometries[0].adjust(adjustCfg); - view.axis(VIOLIN_Y_FIELD, { - grid: { - line: null, - }, - tickLine: { - alignTick: false, - }, - }); - view.axis(VIOLIN_Y_FIELD, { - grid: { - line: { - style: { - lineWidth: 0.5, - // TODO: 为什么是 dash ? - lineDash: [4, 4], - }, - }, - }, - }); - return params; } @@ -226,16 +207,47 @@ function meta(params: Params): Params { */ function axis(params: Params): Params { const { chart, options } = params; - const { xAxis, yAxis, xField, yField, tooltip } = options; + const { xAxis, yAxis } = options; + + const view = findViewById(chart, VIOLIN_VIEW_ID); // 为 false 则是不显示轴 if (xAxis === false) { - chart.axis(xField, false); + view.axis(X_FIELD, false); + } else { + view.axis(X_FIELD, xAxis); + } + + if (yAxis === false) { + view.axis(VIOLIN_Y_FIELD, false); } else { - chart.axis(xField, xAxis); + view.axis(VIOLIN_Y_FIELD, yAxis); } - chart.axis(yField, yAxis); + chart.axis(false); + + return params; +} + +/** + * + * @param params + * @returns + */ +function legend(params: Params): Params { + const { chart, options } = params; + const { legend, seriesField } = options; + + if (legend === false) { + chart.legend(false); + } else { + const legendField = seriesField ? SERIES_FIELD : X_FIELD; + chart.legend(legendField, omit(legend, ['selected'])); + // 特殊的处理 fixme G2 层得解决这个问题 + if (get(legend, 'selected')) { + each(chart.views, (view) => view.legend(legendField, legend)); + } + } return params; } @@ -321,5 +333,5 @@ function tooltip(params: Params): Params { * @param params */ export function adaptor(params: Params) { - return flow(data, violinView, boxView, meta, tooltip, axis, interaction, animation, theme)(params); + return flow(data, violinView, boxView, meta, tooltip, axis, legend, interaction, animation, theme)(params); } diff --git a/src/plots/violin/constant.ts b/src/plots/violin/constant.ts index 97f54561d4..ea84d349a0 100644 --- a/src/plots/violin/constant.ts +++ b/src/plots/violin/constant.ts @@ -46,6 +46,28 @@ export const DEFAULT_OPTIONS = deepAssign({}, Plot.getDefaultOptions(), { strokeOpacity: 0.75, }, + xAxis: { + grid: { + line: null, + }, + tickLine: { + alignTick: false, + }, + }, + yAxis: { + grid: { + line: { + style: { + lineWidth: 0.5, + lineDash: [4, 4], + }, + }, + }, + }, + legend: { + position: 'top-left', + }, + // 默认区域交互 // interactions: [{ type: 'active-region' }], } as Partial); From feaee82b3b17430097ed70de47004c1984f4bc57 Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Sat, 29 May 2021 12:46:23 +0800 Subject: [PATCH 04/18] =?UTF-8?q?fix(violin):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=B0=8F=E6=8F=90=E7=90=B4=E5=9B=BE=E5=8D=95=E6=B5=8B=E6=8A=A5?= =?UTF-8?q?=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/unit/plots/violin/legend-spec.ts | 30 ++++++++++++++++++++++ src/plots/violin/adaptor.ts | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 __tests__/unit/plots/violin/legend-spec.ts diff --git a/__tests__/unit/plots/violin/legend-spec.ts b/__tests__/unit/plots/violin/legend-spec.ts new file mode 100644 index 0000000000..b6f6af0ad2 --- /dev/null +++ b/__tests__/unit/plots/violin/legend-spec.ts @@ -0,0 +1,30 @@ +import { group } from '@antv/util'; +import { Violin } from '../../../../src'; +import { VIOLIN_VIEW_ID } from '../../../../src/plots/violin/constant'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin', () => { + it('没有 seriesField', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + const g = violin.chart.views.find((view) => view.id === VIOLIN_VIEW_ID).geometries[0]; + + // 个数 + expect(g.elements.length).toBe(group(BASE_VIOLIN_DATA, 'type').length); + // 类型 + expect(g.type).toBe('violin'); + + const legendItems = violin.chart.getController('legend').getComponents()[0].component.get('items'); + expect(legendItems.length).toBe(4); + + violin.destroy(); + }); +}); diff --git a/src/plots/violin/adaptor.ts b/src/plots/violin/adaptor.ts index aa2e05d934..21084fde77 100644 --- a/src/plots/violin/adaptor.ts +++ b/src/plots/violin/adaptor.ts @@ -242,7 +242,7 @@ function legend(params: Params): Params { chart.legend(false); } else { const legendField = seriesField ? SERIES_FIELD : X_FIELD; - chart.legend(legendField, omit(legend, ['selected'])); + chart.legend(legendField, omit(legend as any, ['selected'])); // 特殊的处理 fixme G2 层得解决这个问题 if (get(legend, 'selected')) { each(chart.views, (view) => view.legend(legendField, legend)); From 5a65110b27346d04e5ef5e42920cfc4d887c07de Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Sat, 29 May 2021 13:08:44 +0800 Subject: [PATCH 05/18] =?UTF-8?q?test(violin):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=B0=8F=E6=8F=90=E7=90=B4=E5=9B=BE=20legend=20&=20axis=20?= =?UTF-8?q?=E5=8D=95=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/unit/plots/violin/axis-spec.ts | 65 +++++++++++++ __tests__/unit/plots/violin/legend-spec.ts | 107 +++++++++++++++++++-- 2 files changed, 162 insertions(+), 10 deletions(-) create mode 100644 __tests__/unit/plots/violin/axis-spec.ts diff --git a/__tests__/unit/plots/violin/axis-spec.ts b/__tests__/unit/plots/violin/axis-spec.ts new file mode 100644 index 0000000000..3d93a6c0bc --- /dev/null +++ b/__tests__/unit/plots/violin/axis-spec.ts @@ -0,0 +1,65 @@ +import { Violin } from '../../../../src'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin axis', () => { + it('没有 seriesField', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + + let axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents().length).toBe(4); + + violin.update({ xAxis: { grid: null } }); + axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents().length).toBe(3); + + violin.update({ xAxis: { title: { text: 'xx' } } }); + axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents()[0].component.get('title').text).toBe('xx'); + + // 关闭 xAxis + violin.update({ xAxis: false }); + axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents().length).toBe(2); + + violin.destroy(); + }); + + it('有 seriesField', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + seriesField: 'species', + }); + + violin.render(); + let axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents().length).toBe(4); + + violin.update({ xAxis: { grid: null } }); + axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents().length).toBe(3); + + violin.update({ xAxis: { title: { text: 'xx' } } }); + axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents()[0].component.get('title').text).toBe('xx'); + + // 关闭 xAxis + violin.update({ xAxis: false }); + axisController = violin.chart.views[0].getController('axis'); + expect(axisController.getComponents().length).toBe(2); + + violin.destroy(); + }); +}); diff --git a/__tests__/unit/plots/violin/legend-spec.ts b/__tests__/unit/plots/violin/legend-spec.ts index b6f6af0ad2..5a606247a8 100644 --- a/__tests__/unit/plots/violin/legend-spec.ts +++ b/__tests__/unit/plots/violin/legend-spec.ts @@ -1,10 +1,8 @@ -import { group } from '@antv/util'; import { Violin } from '../../../../src'; -import { VIOLIN_VIEW_ID } from '../../../../src/plots/violin/constant'; import { BASE_VIOLIN_DATA } from '../../../data/violin'; import { createDiv } from '../../../utils/dom'; -describe('violin', () => { +describe('violin legend', () => { it('没有 seriesField', () => { const violin = new Violin(createDiv(), { width: 400, @@ -15,16 +13,105 @@ describe('violin', () => { }); violin.render(); - const g = violin.chart.views.find((view) => view.id === VIOLIN_VIEW_ID).geometries[0]; - // 个数 - expect(g.elements.length).toBe(group(BASE_VIOLIN_DATA, 'type').length); - // 类型 - expect(g.type).toBe('violin'); - - const legendItems = violin.chart.getController('legend').getComponents()[0].component.get('items'); + const legendController = violin.chart.getController('legend'); + const legendComponent = legendController.getComponents()[0].component; + let legendItems = legendComponent.get('items'); expect(legendItems.length).toBe(4); + violin.update({ legend: { position: 'left' } }); + expect(legendController.getComponents()[0].direction).toBe('left'); + + // 自定义 legend + violin.update({ + legend: { + custom: true, + position: 'bottom', + items: [ + { + value: '1', + name: '3', + marker: { symbol: 'square', style: { fill: 'red', r: 5 } }, + }, + { + value: '2', + name: '3', + marker: { symbol: 'square', style: { fill: '#000', r: 5 } }, + }, + { + value: '3', + name: '3', + marker: { symbol: 'circle', style: { stroke: '#eee', r: 5 } }, + }, + ], + }, + }); + expect(legendController.getComponents()[0].direction).toBe('bottom'); + legendItems = legendController.getComponents()[0].component.get('items'); + expect(legendItems.length).toBe(3); + expect(legendItems[0].marker.symbol).toBe('square'); + expect(legendItems[0].marker.style.fill).toBe('red'); + expect(legendItems[2].marker.style.stroke).toBe('#eee'); + expect(legendItems[2].marker.symbol).toBe('circle'); + + // 关闭 legend + violin.update({ legend: false }); + expect(legendComponent.get('items')).toBeUndefined(); + + violin.destroy(); + }); + + it('有 seriesField', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + seriesField: 'species', + }); + + violin.render(); + + const legendController = violin.chart.getController('legend'); + const legendComponent = legendController.getComponents()[0].component; + let legendItems = legendComponent.get('items'); + expect(legendItems.length).toBe(3); + + violin.update({ legend: { position: 'left' } }); + expect(legendController.getComponents()[0].direction).toBe('left'); + + // 自定义 legend + violin.update({ + legend: { + custom: true, + position: 'bottom', + items: [ + { + value: '1', + name: '3', + marker: { symbol: 'square', style: { fill: 'red', r: 5 } }, + }, + { + value: '3', + name: '3', + marker: { symbol: 'circle', style: { stroke: '#eee', r: 5 } }, + }, + ], + }, + }); + expect(legendController.getComponents()[0].direction).toBe('bottom'); + legendItems = legendController.getComponents()[0].component.get('items'); + expect(legendItems.length).toBe(2); + expect(legendItems[0].marker.symbol).toBe('square'); + expect(legendItems[0].marker.style.fill).toBe('red'); + expect(legendItems[1].marker.style.stroke).toBe('#eee'); + expect(legendItems[1].marker.symbol).toBe('circle'); + + // 关闭 legend + violin.update({ legend: false }); + expect(legendComponent.get('items')).toBeUndefined(); + violin.destroy(); }); }); From 6260b42206d7c514eb319c15e1d87ff26e7f8e58 Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Sat, 29 May 2021 14:13:05 +0800 Subject: [PATCH 06/18] =?UTF-8?q?refactor(violin):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=B0=8F=E6=8F=90=E7=90=B4=E5=9B=BE=20tooltip=20&=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E6=97=A0=E7=94=A8=20series=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/unit/plots/violin/tooltip-spec.ts | 96 +++++++++++++++++ __tests__/unit/plots/violin/utils-spec.ts | 4 + docs/api/plots/violin.en.md | 48 +++------ docs/api/plots/violin.zh.md | 58 ++++------ examples/more-plots/violin/demo/tooltip.ts | 40 +++---- src/plots/violin/adaptor.ts | 114 +++----------------- src/plots/violin/constant.ts | 9 +- src/plots/violin/types.ts | 18 ---- src/plots/violin/utils.ts | 24 +++-- 9 files changed, 196 insertions(+), 215 deletions(-) create mode 100644 __tests__/unit/plots/violin/tooltip-spec.ts diff --git a/__tests__/unit/plots/violin/tooltip-spec.ts b/__tests__/unit/plots/violin/tooltip-spec.ts new file mode 100644 index 0000000000..a7b23c31ca --- /dev/null +++ b/__tests__/unit/plots/violin/tooltip-spec.ts @@ -0,0 +1,96 @@ +import { Violin } from '../../../../src'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin tooltip', () => { + const div = createDiv(); + const violin = new Violin(div, { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + + it('default', () => { + const box = violin.chart.views[0].geometries[0].elements[0].shape.getBBox(); + const point = { x: box.x + box.width / 2, y: box.y + box.height / 2 }; + + violin.chart.showTooltip(point); + expect(div.querySelectorAll('.g2-tooltip-list-item').length).toBe(5); + violin.chart.hideTooltip(); + }); + + it('tooltip: fields', () => { + violin.update({ + seriesField: 'species', + tooltip: { + fields: ['species', 'high', 'q1', 'q3', 'low', 'median'], + }, + }); + const box = violin.chart.views[0].geometries[0].elements[0].shape.getBBox(); + const point = { x: box.x + box.width / 2, y: box.y + box.height / 2 }; + violin.chart.showTooltip(point); + expect(div.querySelectorAll('.g2-tooltip-list-item').length).toBe(6); + violin.chart.hideTooltip(); + }); + + it('tooltip + meta', () => { + violin.update({ + meta: { + high: { + alias: '最大值', + formatter: (v) => `${v.toFixed(2)} %`, + }, + low: { + alias: '最小值', + formatter: (v) => `${v.toFixed(2)} %`, + }, + species: { + alias: '品类', + }, + }, + }); + + const box = violin.chart.views[0].geometries[0].elements[0].shape.getBBox(); + const point = { x: box.x + box.width / 2, y: box.y + box.height / 2 }; + violin.chart.showTooltip(point); + const items = div.querySelectorAll('.g2-tooltip-name'); + expect((items[0] as HTMLElement).innerText).toBe('品类'); + expect((items[1] as HTMLElement).innerText).toBe('最大值'); + expect((items[4] as HTMLElement).innerText).toBe('最小值'); + expect((div.querySelectorAll('.g2-tooltip-value')[4] as HTMLElement).innerText).toMatch('%'); + violin.chart.hideTooltip(); + }); + + it('tooltip: customContent', () => { + violin.update({ + tooltip: { + customContent: () => '
xxx
', + }, + }); + const box = violin.chart.views[0].geometries[0].elements[0].shape.getBBox(); + const point = { x: box.x + box.width / 2, y: box.y + box.height / 2 }; + violin.chart.showTooltip(point); + expect((div.querySelector('.custom-tooltip') as HTMLElement).innerText).toBe('xxx'); + violin.chart.hideTooltip(); + }); + + it('tooltip: false', () => { + // @ts-ignore + expect(violin.chart.options.tooltip).not.toBe(false); + // @ts-ignore + expect(violin.chart.getController('tooltip').isVisible()).toBe(true); + violin.update({ tooltip: false }); + // @ts-ignore + expect(violin.chart.options.tooltip).toBe(false); + // @ts-ignore + expect(violin.chart.getController('tooltip').isVisible()).toBe(false); + }); + + afterAll(() => { + violin.destroy(); + }); +}); diff --git a/__tests__/unit/plots/violin/utils-spec.ts b/__tests__/unit/plots/violin/utils-spec.ts index fcec23bd81..d4aac2eded 100644 --- a/__tests__/unit/plots/violin/utils-spec.ts +++ b/__tests__/unit/plots/violin/utils-spec.ts @@ -13,6 +13,10 @@ describe('violin utils', () => { // 随机打乱,不应影响计算结果。 const shuffled = ordered.slice().sort(() => Math.random() - 0.5); expect(toBoxValue(shuffled)).toEqual({ + high: 100, + low: -1, + q1: 3, + q3: 8, minMax: [-1, 100], quantile: [3, 8], median: [4.4], diff --git a/docs/api/plots/violin.en.md b/docs/api/plots/violin.en.md index e835a74757..08a134da62 100644 --- a/docs/api/plots/violin.en.md +++ b/docs/api/plots/violin.en.md @@ -49,6 +49,20 @@ type KdeOptions = { `markdown:docs/common/meta.en.md` +小提琴图内置的箱线图配置。设置为 `false` 时不渲染箱线图。 + +箱线图的统计数据分别为: + +- high: 数据中的最大值,作为箱线图的最高点; +- low: 数据中的最小值,作为箱线图的最低点; +- q3: 上四分位,即 25% 的数据大于该数,作为箱线图中箱子的高点; +- q1: 下四分位,即 25% 的数据小于该数,作为箱线图中箱子的低点; +- median: 数据的中位数,在箱线图中用圆点表示。 + +可以通过 `meta` 来设置字段的元信息 + + + ### Graphic Style #### shape @@ -65,40 +79,6 @@ Violin graphic style. `markdown:docs/common/shape-style.en.md` -### box - -**optional** _false | object_ - -Options to render inner box plot. Set `false` to avoid rendering box plot. - -The statistical data of inner box plot includes: - -- max: The maximum value, rendered as the highest point in the box plot. -- min: The minimum value, rendered as the lowest point in the box plot. -- q3: The 3rd quartile, rendered as box top. -- q1: The 1st quartile, rendered as box bottom. -- median: The median value, rendered as a little circle. - -You can specify these texts in `box.textMap`. - -```ts -type BoxOptions = false | { - /** Text of the box plot. */ - textMap?: { - /** Max value label. */ - max: string; - /** Min value label. */ - min: string; - /** 1st quantile value label. */ - q1: string; - /** 3rd quantile value label. */ - q3: string; - /** Median value label. */ - median: string; - }; -}; -``` - `markdown:docs/common/color.en.md` ### Plot Components diff --git a/docs/api/plots/violin.zh.md b/docs/api/plots/violin.zh.md index 6dce5057d0..83e7ef79b3 100644 --- a/docs/api/plots/violin.zh.md +++ b/docs/api/plots/violin.zh.md @@ -48,6 +48,28 @@ type KdeOptions = { `markdown:docs/common/meta.zh.md` + +### box + +**optional** _false | object_ + +小提琴图内置的箱线图配置。设置为 `false` 时不渲染箱线图。 + +箱线图的统计数据分别为: + +- high: 数据中的最大值,作为箱线图的最高点; +- low: 数据中的最小值,作为箱线图的最低点; +- q3: 上四分位,即 25% 的数据大于该数,作为箱线图中箱子的高点; +- q1: 下四分位,即 25% 的数据小于该数,作为箱线图中箱子的低点; +- median: 数据的中位数,在箱线图中用圆点表示。 + +可以通过 `meta` 来设置字段的元信息 + + + +`markdown:docs/common/color.zh.md` + + ### 图形样式 #### shape @@ -68,42 +90,6 @@ type KdeOptions = { `markdown:docs/common/shape-style.zh.md` -### box - -**optional** _false | object_ - -小提琴图内置的箱线图配置。设置为 `false` 时不渲染箱线图。 - -箱线图的统计数据分别为: - -- max: 数据中的最大值,作为箱线图的最高点; -- min: 数据中的最小值,作为箱线图的最低点; -- q3: 上四分位,即 25% 的数据大于该数,作为箱线图中箱子的高点; -- q1: 下四分位,即 25% 的数据小于该数,作为箱线图中箱子的低点; -- median: 数据的中位数,在箱线图中用圆点表示。 - -可以在 `box.textMap` 中指定文案。 - -```ts -type BoxOptions = false | { - /** 箱线图的文案映射 */ - textMap?: { - /** 最大值文案 */ - max: string; - /** 最小值文案 */ - min: string; - /** 下四分位数文案 */ - q1: string; - /** 上四分位数文案 */ - q3: string; - /** 中位数文案 */ - median: string; - }; -}; -``` - -`markdown:docs/common/color.zh.md` - ### 图表组件 `markdown:docs/common/component.zh.md` diff --git a/examples/more-plots/violin/demo/tooltip.ts b/examples/more-plots/violin/demo/tooltip.ts index bc29924628..968ffab671 100644 --- a/examples/more-plots/violin/demo/tooltip.ts +++ b/examples/more-plots/violin/demo/tooltip.ts @@ -4,31 +4,35 @@ fetch('https://gw.alipayobjects.com/os/bmw-prod/6b0a5f1d-5931-42ae-b3ba-3c3cb77d .then((response) => response.json()) .then((data) => { const violinPlot = new Violin('container', { + width: 400, height: 500, data: data, xField: 'x', yField: 'y', - box: { - textMap: { - max: '最大值', - min: '最小值', - q3: '上四分位', - q1: '下四分位', - median: '中位数', + seriesField: 'species', + meta: { + high: { + alias: '最大值', + formatter: (v) => `${v.toFixed(2)} %`, + }, + low: { + alias: '最小值', + formatter: (v) => `${v.toFixed(2)} %`, + }, + q1: { + alias: '上四分位数', + formatter: (v) => `${v.toFixed(2)} %`, + }, + q3: { + alias: '下四分位数', + formatter: (v) => `${v.toFixed(2)} %`, + }, + species: { + alias: '品类', }, }, tooltip: { - formatter: (datum) => { - return { - value: { - max: datum.minMax[0] + '%', - min: datum.minMax[1] + '%', - q1: datum.quantile[0] + '%', - q3: datum.quantile[1] + '%', - median: datum.median[0] + '%', - }, - }; - }, + fields: ['species', 'high', 'q1', 'q3', 'low'], }, }); violinPlot.render(); diff --git a/src/plots/violin/adaptor.ts b/src/plots/violin/adaptor.ts index 21084fde77..57dd37e938 100644 --- a/src/plots/violin/adaptor.ts +++ b/src/plots/violin/adaptor.ts @@ -1,10 +1,9 @@ import { get, omit, each } from '@antv/util'; import { Params } from '../../core/adaptor'; -import { interaction, animation, theme } from '../../adaptor/common'; +import { interaction, animation, theme, tooltip } from '../../adaptor/common'; import { interval, point, violin } from '../../adaptor/geometries'; import { flow, pick, deepAssign, findViewById } from '../../utils'; import { AXIS_META_CONFIG_KEYS } from '../../constant'; -import { Datum } from '../../types'; import { ViolinOptions } from './types'; import { transformViolinData } from './utils'; import { @@ -14,22 +13,13 @@ import { MIN_MAX_VIEW_ID, QUANTILE_FIELD, QUANTILE_VIEW_ID, - SERIES_FIELD, VIOLIN_SIZE_FIELD, VIOLIN_VIEW_ID, VIOLIN_Y_FIELD, X_FIELD, } from './constant'; -const ALL_FIELDS = [ - X_FIELD, - SERIES_FIELD, - VIOLIN_Y_FIELD, - VIOLIN_SIZE_FIELD, - MIN_MAX_FIELD, - QUANTILE_FIELD, - MEDIAN_FIELD, -]; +const TOOLTIP_FIELDS = ['low', 'high', 'q1', 'q3', 'median']; const adjustCfg = [ { @@ -56,10 +46,10 @@ function violinView(params: Params): Params { options: { xField: X_FIELD, yField: VIOLIN_Y_FIELD, - seriesField: seriesField ? SERIES_FIELD : X_FIELD, + seriesField: seriesField ? seriesField : X_FIELD, sizeField: VIOLIN_SIZE_FIELD, tooltip: { - fields: ALL_FIELDS, + fields: TOOLTIP_FIELDS, ...tooltip, }, violin: { @@ -77,10 +67,10 @@ function violinView(params: Params): Params { /** 箱线 */ function boxView(params: Params): Params { const { chart, options } = params; - const { seriesField, color, box, tooltip } = options; + const { seriesField, color, tooltip } = options; - // 如果配置 `box` 为 false ,不渲染内部箱线图 - if (!box) return params; + // 如果配置 `box` 为 false ,不渲染内部箱线图 (暂时不开放 关闭) + // if (!box) return params; // 边缘线 const minMaxView = chart.createView({ id: MIN_MAX_VIEW_ID }); @@ -89,9 +79,9 @@ function boxView(params: Params): Params { options: { xField: X_FIELD, yField: MIN_MAX_FIELD, - seriesField: seriesField ? SERIES_FIELD : X_FIELD, + seriesField: seriesField ? seriesField : X_FIELD, tooltip: { - fields: ALL_FIELDS, + fields: TOOLTIP_FIELDS, ...tooltip, }, interval: { @@ -112,9 +102,9 @@ function boxView(params: Params): Params { options: { xField: X_FIELD, yField: QUANTILE_FIELD, - seriesField: seriesField ? SERIES_FIELD : X_FIELD, + seriesField: seriesField ? seriesField : X_FIELD, tooltip: { - fields: ALL_FIELDS, + fields: TOOLTIP_FIELDS, ...tooltip, }, interval: { @@ -135,9 +125,9 @@ function boxView(params: Params): Params { options: { xField: X_FIELD, yField: MEDIAN_FIELD, - seriesField: seriesField ? SERIES_FIELD : X_FIELD, + seriesField: seriesField ? seriesField : X_FIELD, tooltip: { - fields: ALL_FIELDS, + fields: TOOLTIP_FIELDS, ...tooltip, }, point: { @@ -241,7 +231,7 @@ function legend(params: Params): Params { if (legend === false) { chart.legend(false); } else { - const legendField = seriesField ? SERIES_FIELD : X_FIELD; + const legendField = seriesField ? seriesField : X_FIELD; chart.legend(legendField, omit(legend as any, ['selected'])); // 特殊的处理 fixme G2 层得解决这个问题 if (get(legend, 'selected')) { @@ -252,82 +242,6 @@ function legend(params: Params): Params { return params; } -function tooltip(params: Params): Params { - const { chart, options } = params; - const { box } = options; - if (!box) return params; - - const textMap = box.textMap; - chart.tooltip( - deepAssign( - {}, - // 内置的配置 - { - showMarkers: false, - // 默认 formatter 把 datum 转换为 { value: { min, max, q1, q3, median } } 的结构体。 - formatter: (datum: Datum) => { - return { - value: { - min: datum.minMax[0], - max: datum.minMax[1], - q1: datum.quantile[0], - q3: datum.quantile[1], - median: datum.median[0], - }, - }; - }, - // 默认 customItems 消费上述结构体。 - customItems: (originalItems) => { - const sample = originalItems?.[0]; - if (!sample) return []; - - return [ - { - ...sample, - name: textMap.max, - title: textMap.max, - value: sample.value.max, - marker: 'circle', - }, - { - ...sample, - name: textMap.q3, - title: textMap.q3, - value: sample.value.q3, - marker: 'circle', - }, - { - ...sample, - name: textMap.median, - title: textMap.median, - value: sample.value.median, - marker: 'circle', - }, - { - ...sample, - name: textMap.q1, - title: textMap.q1, - value: sample.value.q1, - marker: 'circle', - }, - { - ...sample, - name: textMap.min, - title: textMap.min, - value: sample.value.min, - marker: 'circle', - }, - ]; - }, - }, - // 用户的配置 - options.tooltip - ) - ); - - return params; -} - /** * 箱型图适配器 * @param params diff --git a/src/plots/violin/constant.ts b/src/plots/violin/constant.ts index ea84d349a0..c1f9cc6f0b 100644 --- a/src/plots/violin/constant.ts +++ b/src/plots/violin/constant.ts @@ -3,7 +3,6 @@ import { deepAssign } from '../../utils'; import { ViolinOptions } from './types'; export const X_FIELD = 'x'; -export const SERIES_FIELD = 'series'; export const VIOLIN_Y_FIELD = 'violinY'; export const VIOLIN_SIZE_FIELD = 'violinSize'; export const MIN_MAX_FIELD = 'minMax'; @@ -45,7 +44,7 @@ export const DEFAULT_OPTIONS = deepAssign({}, Plot.getDefaultOptions(), { fillOpacity: 0.3, strokeOpacity: 0.75, }, - + // 坐标轴 xAxis: { grid: { line: null, @@ -64,10 +63,14 @@ export const DEFAULT_OPTIONS = deepAssign({}, Plot.getDefaultOptions(), { }, }, }, + // 图例 legend: { position: 'top-left', }, - + // Tooltip + tooltip: { + showMarkers: false, + }, // 默认区域交互 // interactions: [{ type: 'active-region' }], } as Partial); diff --git a/src/plots/violin/types.ts b/src/plots/violin/types.ts index 9cbb65825f..b413d83e7b 100644 --- a/src/plots/violin/types.ts +++ b/src/plots/violin/types.ts @@ -30,22 +30,4 @@ export interface ViolinOptions extends Options { /** 核函数的带宽。带宽越大产生的曲线越平滑(越模糊),带宽越小产生的曲线越陡峭。默认3。 */ width?: number; } /* | { type: 'gaussian', } ⬅️ 像这样添加新的核函数支持 */; - /** 内部箱线图配置,false 为不显示。 */ - readonly box?: - | false - | { - /** 箱线图的文案映射 */ - textMap?: { - /** 最大值文案 */ - max: string; - /** 最小值文案 */ - min: string; - /** 下四分位数文案 */ - q1: string; - /** 上四分位数文案 */ - q3: string; - /** 中位数文案 */ - median: string; - }; - }; } diff --git a/src/plots/violin/utils.ts b/src/plots/violin/utils.ts index 4c56728be0..75d4bab343 100644 --- a/src/plots/violin/utils.ts +++ b/src/plots/violin/utils.ts @@ -6,20 +6,28 @@ import { ViolinOptions } from './types'; export type ViolinData = { /** X轴 */ x: string; - /** 拆分 */ - series?: string; /** 小提琴轮廓的 size 通道数据 */ violinSize: number[]; /** 小提琴轮廓的 y 通道数据 */ violinY: number[]; + // 箱线图基础数据 + /** 最大值 */ + high: number; + /** 最小值 */ + low: number; + /** 上四分位数 */ + q1: number; + /** 下四分位数 */ + q3: number; + /** 箱线图中的中位值 */ + median: number[]; + /** 箱线图中的上线边缘线 */ minMax: number[]; /** 箱线图中的上下四分位点 */ quantile: number[]; - /** 箱线图中的中位值 */ - median: number[]; }; export type PdfOptions = { @@ -31,9 +39,13 @@ export type PdfOptions = { export const toBoxValue = (values: number[]) => { return { + low: min(values), + high: max(values), + q1: quantile(values, 0.25), + q3: quantile(values, 0.75), + median: quantile(values, [0.5]), minMax: [min(values), max(values)], quantile: [quantile(values, 0.25), quantile(values, 0.75)], - median: quantile(values, [0.5]), }; }; @@ -80,7 +92,7 @@ export const transformViolinData = (options: ViolinOptions): ViolinData[] => { const values = records.map((record) => record[yField]); resultList.push({ x: key, - series, + [seriesField]: series, ...toViolinValue(values, pdfOptions), ...toBoxValue(values), }); From 3d30d0ff3b7dbf062798058bb97e3e75e902bc93 Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Sun, 30 May 2021 12:43:34 +0800 Subject: [PATCH 07/18] =?UTF-8?q?chore:=20=E5=A2=9E=E5=A4=A7=E5=8C=85?= =?UTF-8?q?=E9=99=90=E5=88=B6=E5=88=B0=20910kb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e8a9241fec..318ccac52d 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "limit-size": [ { "path": "dist/g2plot.min.js", - "limit": "900 Kb" + "limit": "910 Kb" }, { "path": "dist/g2plot.min.js", From 1c4ecbed8f4501b52474dd70e531dca9f94e46c5 Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Sun, 30 May 2021 12:44:05 +0800 Subject: [PATCH 08/18] =?UTF-8?q?test(violin):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=8D=95=E6=B5=8B=E9=97=AE=E9=A2=98=20&=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=8D=95=E6=B5=8B=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/unit/plots/violin/box-spec.ts | 32 ++---------- .../unit/utils/transform/quantile-spec.ts | 50 +++++++++++++++++++ src/utils/transform/quantile.ts | 12 +++-- 3 files changed, 62 insertions(+), 32 deletions(-) create mode 100644 __tests__/unit/utils/transform/quantile-spec.ts diff --git a/__tests__/unit/plots/violin/box-spec.ts b/__tests__/unit/plots/violin/box-spec.ts index b02c151705..ec049e4c3c 100644 --- a/__tests__/unit/plots/violin/box-spec.ts +++ b/__tests__/unit/plots/violin/box-spec.ts @@ -25,14 +25,15 @@ describe('violin', () => { violin.destroy(); }); - it("should not render box views when 'box' set to false.", () => { + // 暂时不开放 box 配置 + it.skip("should not render box views when 'box' set to false.", () => { const violin = new Violin(createDiv(), { width: 400, height: 500, data: BASE_VIOLIN_DATA, xField: 'type', yField: 'value', - box: false, + // box: false, }); violin.render(); @@ -46,31 +47,4 @@ describe('violin', () => { violin.destroy(); }); - - it('should not render box with custom textMap.', () => { - const textMap = { - max: '最大值', - min: '最小值', - median: '中位值', - q1: '上四分位点', - q3: '下四分位点', - }; - const violin = new Violin(createDiv(), { - width: 400, - height: 500, - data: BASE_VIOLIN_DATA, - xField: 'type', - yField: 'value', - box: { - textMap, - }, - }); - - violin.render(); - - // @ts-ignore - expect(violin.options.box.textMap).toEqual(textMap); - - violin.destroy(); - }); }); diff --git a/__tests__/unit/utils/transform/quantile-spec.ts b/__tests__/unit/utils/transform/quantile-spec.ts new file mode 100644 index 0000000000..68f095601b --- /dev/null +++ b/__tests__/unit/utils/transform/quantile-spec.ts @@ -0,0 +1,50 @@ +import { quantile, quantileSorted, quickselect, swap } from '../../../../src/utils/transform/quantile'; + +describe('quantile', () => { + it('quantile-sorted', () => { + expect(quantileSorted([3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 0.5)).toBe(9); + }); + + it('quick-select', () => { + const arr = [65, 28, 59, 33, 21, 56, 22, 95, 50, 12, 90, 53, 28, 77, 39]; + quickselect(arr, 8); + expect(arr).toEqual([39, 28, 28, 33, 21, 12, 22, 50, 53, 56, 59, 65, 90, 77, 95]); + }); + + it('quantile', () => { + expect(quantile([3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 0.5)).toBe(9); // => 9 + + // from https://github.com/simple-statistics + const even = [3, 6, 7, 8, 8, 10, 13, 15, 16, 20]; + expect(quantile(even, 0.25)).toBe(7); + expect(quantile(even, 0.5)).toBe(9); + expect(quantile(even, 0.75)).toBe(15); + + const odd = [3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20]; + expect(quantile(odd, 0.25)).toBe(7); + expect(quantile(odd, 0.5)).toBe(9); + expect(quantile(odd, 0.75)).toBe(15); + }); + + it('quantile: throw error', () => { + const testEmptyArray = () => quantile([], 0.5); + expect(testEmptyArray).toThrowError(); + + const testBadBounds = () => quantile([1, 2, 3], 1.1); + expect(testBadBounds).toThrowError(); + }); + + it('quantile: can get an array of quantiles on a small number of elements', () => { + const input = [500, 468, 454, 469]; + expect(quantile(input, [0.25, 0.5, 0.75])).toEqual([461, 468.5, 484.5]); + expect(quantile(input, [0.05, 0.25, 0.5, 0.75, 0.95])).toEqual([454, 461, 468.5, 484.5, 500]); + }); + + it('swap', () => { + const arr = [1, 2, 3]; + swap(arr, 0, 2); + expect(arr).toEqual([3, 2, 1]); + swap(arr, 0, 2); + expect(arr).toEqual([1, 2, 3]); + }); +}); diff --git a/src/utils/transform/quantile.ts b/src/utils/transform/quantile.ts index a99d822e98..a57547482b 100644 --- a/src/utils/transform/quantile.ts +++ b/src/utils/transform/quantile.ts @@ -13,7 +13,7 @@ * @example * quantileSorted([3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 0.5); // => 9 */ -function quantileSorted(x, p) { +export function quantileSorted(x: number[], p: number) { const idx = x.length * p; if (x.length === 0) { throw new Error('quantile requires at least one data point.'); @@ -39,7 +39,13 @@ function quantileSorted(x, p) { } } -function swap(arr, i, j) { +/** + * 交换数组位置 + * @param arr T[] + * @param i number + * @param j number + */ +export function swap(arr: T[], i: number, j: number): void { const tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; @@ -61,7 +67,7 @@ function swap(arr, i, j) { * quickselect(arr, 8); * // = [39, 28, 28, 33, 21, 12, 22, 50, 53, 56, 59, 65, 90, 77, 95] */ -function quickselect(arr, k, left, right) { +export function quickselect(arr: number[], k, left?: number, right?: number): void { left = left || 0; right = right || arr.length - 1; From fbb6f2d0632364328e058b0f1027ce805888f8d3 Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Mon, 31 May 2021 18:06:13 +0800 Subject: [PATCH 09/18] =?UTF-8?q?feat(violin):=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=B0=8F=E6=8F=90=E7=90=B4=E5=9B=BE=E5=8A=A8=E7=94=BB=E5=92=8C?= =?UTF-8?q?=E5=9B=BE=E5=BD=A2=E6=A0=87=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/unit/plots/violin/animation-spec.ts | 85 +++++++++++++++++++ .../unit/plots/violin/annotation-spec.ts | 65 ++++++++++++++ src/plots/violin/adaptor.ts | 58 ++++++++++++- 3 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 __tests__/unit/plots/violin/animation-spec.ts create mode 100644 __tests__/unit/plots/violin/annotation-spec.ts diff --git a/__tests__/unit/plots/violin/animation-spec.ts b/__tests__/unit/plots/violin/animation-spec.ts new file mode 100644 index 0000000000..1d8d520f94 --- /dev/null +++ b/__tests__/unit/plots/violin/animation-spec.ts @@ -0,0 +1,85 @@ +import { Violin } from '../../../../src'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin legend', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + animation: { + enter: { + animation: 'fade-in', + }, + leave: { + animation: 'fade-out', + }, + }, + }); + + violin.render(); + + it('default', () => { + // 新的 geometry violin 暂时未追加 + expect(violin.chart.views[0].geometries[0].animateOption).toEqual({ + enter: { + animation: 'fade-in', + }, + leave: { + animation: 'fade-out', + }, + }); + + // 追加默认的动画配置 + expect(violin.chart.views[1].geometries[0].animateOption).toMatchObject({ + appear: { + duration: 450, + easing: 'easeQuadOut', + }, + update: { + duration: 400, + easing: 'easeQuadInOut', + }, + enter: { + duration: 400, + easing: 'easeQuadInOut', + animation: 'fade-in', + }, + leave: { + duration: 350, + easing: 'easeQuadIn', + animation: 'fade-out', + }, + }); + }); + + it('update', () => { + violin.update({ + animation: { + appear: { + animation: 'fade-in', + }, + leave: { + animation: 'wave-out', + }, + }, + }); + expect(violin.chart.views[0].geometries[0].animateOption).toEqual({ + appear: { + animation: 'fade-in', + }, + enter: { + animation: 'fade-in', + }, + leave: { + animation: 'wave-out', + }, + }); + }); + + afterAll(() => { + violin.destroy(); + }); +}); diff --git a/__tests__/unit/plots/violin/annotation-spec.ts b/__tests__/unit/plots/violin/annotation-spec.ts new file mode 100644 index 0000000000..7f3ca86dce --- /dev/null +++ b/__tests__/unit/plots/violin/annotation-spec.ts @@ -0,0 +1,65 @@ +import { Violin } from '../../../../src'; +import { BASE_VIOLIN_DATA } from '../../../data/violin'; +import { createDiv } from '../../../utils/dom'; + +describe('violin legend', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + + it('text annotation', () => { + violin.update({ + annotations: [ + { + type: 'text', + position: ['median', 'median'], + content: '辅助文本', + }, + ], + }); + + const controller = violin.chart.views[0].getController('annotation'); + expect(controller.getComponents().length).toBe(1); + expect(controller.getComponents()[0].component.get('content')).toBe('辅助文本'); + }); + + it('text annotation and line annotation', () => { + violin.update({ + ...violin.options, + annotations: [ + { + type: 'text', + position: ['min', 'median'], + content: '辅助文本', + offsetY: -4, + style: { + textBaseviolin: 'bottom', + }, + }, + { + type: 'line', + start: ['min', 'median'], + end: ['max', 'median'], + style: { + stroke: 'red', + violinDash: [2, 2], + }, + }, + ], + }); + const controller = violin.chart.views[0].getController('annotation'); + expect(controller.getComponents().length).toBe(2); + expect(controller.getComponents()[0].component.get('content')).toBe('辅助文本'); + expect(controller.getComponents()[1].component.get('type')).toBe('line'); + }); + + afterAll(() => { + violin.destroy(); + }); +}); diff --git a/src/plots/violin/adaptor.ts b/src/plots/violin/adaptor.ts index 57dd37e938..7c1fecf52b 100644 --- a/src/plots/violin/adaptor.ts +++ b/src/plots/violin/adaptor.ts @@ -1,6 +1,7 @@ +import { Geometry } from '@antv/g2'; import { get, omit, each } from '@antv/util'; import { Params } from '../../core/adaptor'; -import { interaction, animation, theme, tooltip } from '../../adaptor/common'; +import { interaction, theme, tooltip, annotation as baseAnnotation } from '../../adaptor/common'; import { interval, point, violin } from '../../adaptor/geometries'; import { flow, pick, deepAssign, findViewById } from '../../utils'; import { AXIS_META_CONFIG_KEYS } from '../../constant'; @@ -243,9 +244,60 @@ function legend(params: Params): Params { } /** - * 箱型图适配器 + * annotation, apply to violin view. + * @param params + * @returns + */ +function annotation(params: Params): Params { + const { chart } = params; + + const violinView = findViewById(chart, VIOLIN_VIEW_ID); + baseAnnotation()({ ...params, chart: violinView }); + + return params; +} + +/** + * 动画 + * @param params + */ +export function animation(params: Params): Params { + const { chart, options } = params; + const { animation } = options; + + // 所有的 Geometry 都使用同一动画(各个图形如有区别,自行覆盖) + each(chart.views, (view) => { + // 同时设置整个 view 动画选项 + if (typeof animation === 'boolean') { + view.animate(animation); + } else { + view.animate(true); + } + + each(view.geometries, (g: Geometry) => { + g.animate(animation); + }); + }); + + return params; +} + +/** + * 小提琴图适配器 * @param params */ export function adaptor(params: Params) { - return flow(data, violinView, boxView, meta, tooltip, axis, legend, interaction, animation, theme)(params); + return flow( + theme, + data, + violinView, + boxView, + meta, + tooltip, + axis, + legend, + interaction, + annotation, + animation + )(params); } From 2d88d2beb571aec4beda47c01e6f1f601117be4d Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Tue, 1 Jun 2021 00:47:00 +0800 Subject: [PATCH 10/18] =?UTF-8?q?test(violin):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=B0=8F=E6=8F=90=E7=90=B4=E5=9B=BE=20meta=20=E5=8D=95?= =?UTF-8?q?=E6=B5=8B=20&=20=E5=AE=8C=E5=96=84=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/unit/plots/violin/index-spec.ts | 57 ++++++++++++++++++++++- docs/api/plots/violin.en.md | 4 +- docs/api/plots/violin.zh.md | 9 +--- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/__tests__/unit/plots/violin/index-spec.ts b/__tests__/unit/plots/violin/index-spec.ts index fad3482448..445a1dfed8 100644 --- a/__tests__/unit/plots/violin/index-spec.ts +++ b/__tests__/unit/plots/violin/index-spec.ts @@ -1,6 +1,13 @@ import { group } from '@antv/util'; import { Violin } from '../../../../src'; -import { VIOLIN_VIEW_ID } from '../../../../src/plots/violin/constant'; +import { + MEDIAN_FIELD, + MIN_MAX_FIELD, + QUANTILE_FIELD, + VIOLIN_VIEW_ID, + VIOLIN_Y_FIELD, + X_FIELD, +} from '../../../../src/plots/violin/constant'; import { BASE_VIOLIN_DATA } from '../../../data/violin'; import { createDiv } from '../../../utils/dom'; @@ -45,4 +52,52 @@ describe('violin', () => { violin.destroy(); }); + + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + xAxis: { + tickCount: 5, + }, + yAxis: { + // fixme 设置 min 为 10 不生效 + // min: 10, + minLimit: 10, + max: 20, + }, + }); + + violin.render(); + + it('axis & meta', () => { + const geometry = violin.chart.views[0].geometries[0]; + const geometry1 = violin.chart.views[1].geometries[0]; + const geometry2 = violin.chart.views[2].geometries[0]; + const geometry3 = violin.chart.views[3].geometries[0]; + expect(geometry.scales[X_FIELD].tickCount).toBe(5); + expect(geometry.scales[VIOLIN_Y_FIELD].min).toBe(10); + expect(geometry.scales[VIOLIN_Y_FIELD].max).toBe(20); + + expect(geometry1.scales[MIN_MAX_FIELD].min).toBe(10); + expect(geometry1.scales[MIN_MAX_FIELD].max).toBe(20); + // @ts-ignore + expect(geometry1.scales[MIN_MAX_FIELD].sync).toBe(VIOLIN_Y_FIELD); + + expect(geometry2.scales[QUANTILE_FIELD].min).toBe(10); + expect(geometry2.scales[QUANTILE_FIELD].max).toBe(20); + // @ts-ignore + expect(geometry2.scales[QUANTILE_FIELD].sync).toBe(VIOLIN_Y_FIELD); + + expect(geometry3.scales[MEDIAN_FIELD].min).toBe(10); + expect(geometry3.scales[MEDIAN_FIELD].max).toBe(20); + // @ts-ignore + expect(geometry3.scales[MEDIAN_FIELD].sync).toBe(VIOLIN_Y_FIELD); + }); + + afterAll(() => { + violin.destroy(); + }); }); diff --git a/docs/api/plots/violin.en.md b/docs/api/plots/violin.en.md index 08a134da62..32961c3e1f 100644 --- a/docs/api/plots/violin.en.md +++ b/docs/api/plots/violin.en.md @@ -49,9 +49,7 @@ type KdeOptions = { `markdown:docs/common/meta.en.md` -小提琴图内置的箱线图配置。设置为 `false` 时不渲染箱线图。 - -箱线图的统计数据分别为: +小提琴图内置箱线图配置。箱线图的统计数据分别为: - high: 数据中的最大值,作为箱线图的最高点; - low: 数据中的最小值,作为箱线图的最低点; diff --git a/docs/api/plots/violin.zh.md b/docs/api/plots/violin.zh.md index 83e7ef79b3..f9acef7808 100644 --- a/docs/api/plots/violin.zh.md +++ b/docs/api/plots/violin.zh.md @@ -48,14 +48,7 @@ type KdeOptions = { `markdown:docs/common/meta.zh.md` - -### box - -**optional** _false | object_ - -小提琴图内置的箱线图配置。设置为 `false` 时不渲染箱线图。 - -箱线图的统计数据分别为: +小提琴图内置箱线图配置。箱线图的统计数据分别为: - high: 数据中的最大值,作为箱线图的最高点; - low: 数据中的最小值,作为箱线图的最低点; From 8c8b0f3257946608f68da4fca760354370592393 Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Tue, 1 Jun 2021 00:53:31 +0800 Subject: [PATCH 11/18] =?UTF-8?q?test(violin):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=8D=95=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/unit/plots/violin/index-spec.ts | 14 +++++++++++++ __tests__/unit/plots/violin/legend-spec.ts | 24 ++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/__tests__/unit/plots/violin/index-spec.ts b/__tests__/unit/plots/violin/index-spec.ts index 445a1dfed8..b44abaf4c7 100644 --- a/__tests__/unit/plots/violin/index-spec.ts +++ b/__tests__/unit/plots/violin/index-spec.ts @@ -1,6 +1,7 @@ import { group } from '@antv/util'; import { Violin } from '../../../../src'; import { + DEFAULT_OPTIONS, MEDIAN_FIELD, MIN_MAX_FIELD, QUANTILE_FIELD, @@ -22,6 +23,10 @@ describe('violin', () => { }); violin.render(); + expect(violin.type).toBe('violin'); + // @ts-ignore + expect(violin.getDefaultOptions()).toEqual(Violin.getDefaultOptions()); + const g = violin.chart.views.find((view) => view.id === VIOLIN_VIEW_ID).geometries[0]; // 个数 @@ -63,6 +68,7 @@ describe('violin', () => { tickCount: 5, }, yAxis: { + tickCount: 7, // fixme 设置 min 为 10 不生效 // min: 10, minLimit: 10, @@ -80,23 +86,31 @@ describe('violin', () => { expect(geometry.scales[X_FIELD].tickCount).toBe(5); expect(geometry.scales[VIOLIN_Y_FIELD].min).toBe(10); expect(geometry.scales[VIOLIN_Y_FIELD].max).toBe(20); + expect(geometry.scales[VIOLIN_Y_FIELD].tickCount).toBe(7); expect(geometry1.scales[MIN_MAX_FIELD].min).toBe(10); expect(geometry1.scales[MIN_MAX_FIELD].max).toBe(20); + expect(geometry1.scales[MIN_MAX_FIELD].tickCount).toBe(7); // @ts-ignore expect(geometry1.scales[MIN_MAX_FIELD].sync).toBe(VIOLIN_Y_FIELD); expect(geometry2.scales[QUANTILE_FIELD].min).toBe(10); expect(geometry2.scales[QUANTILE_FIELD].max).toBe(20); + expect(geometry2.scales[QUANTILE_FIELD].tickCount).toBe(7); // @ts-ignore expect(geometry2.scales[QUANTILE_FIELD].sync).toBe(VIOLIN_Y_FIELD); expect(geometry3.scales[MEDIAN_FIELD].min).toBe(10); expect(geometry3.scales[MEDIAN_FIELD].max).toBe(20); + expect(geometry3.scales[MEDIAN_FIELD].tickCount).toBe(7); // @ts-ignore expect(geometry3.scales[MEDIAN_FIELD].sync).toBe(VIOLIN_Y_FIELD); }); + it('defaultOptions 保持从 constants 中获取', () => { + expect(Violin.getDefaultOptions()).toEqual(DEFAULT_OPTIONS); + }); + afterAll(() => { violin.destroy(); }); diff --git a/__tests__/unit/plots/violin/legend-spec.ts b/__tests__/unit/plots/violin/legend-spec.ts index 5a606247a8..569fc64b66 100644 --- a/__tests__/unit/plots/violin/legend-spec.ts +++ b/__tests__/unit/plots/violin/legend-spec.ts @@ -1,4 +1,5 @@ import { Violin } from '../../../../src'; +import { X_FIELD } from '../../../../src/plots/violin/constant'; import { BASE_VIOLIN_DATA } from '../../../data/violin'; import { createDiv } from '../../../utils/dom'; @@ -114,4 +115,27 @@ describe('violin legend', () => { violin.destroy(); }); + + it('除了 view0, 其他没有图例', () => { + const violin = new Violin(createDiv(), { + width: 400, + height: 500, + data: BASE_VIOLIN_DATA, + xField: 'type', + yField: 'value', + }); + + violin.render(); + + violin.chart.views.forEach((view, idx) => { + if (idx !== 0) { + // @ts-ignore + expect(view.options.legends).toBe(false); + } + }); + + violin.update({ legend: { position: 'left', marker: { style: { fill: 'red' } } } }); + // @ts-ignore + expect(violin.chart.options.legends[X_FIELD]).toEqual({ position: 'left', marker: { style: { fill: 'red' } } }); + }); }); From 4db2233bd4875cd4af5a57552b65e0eb3b074283 Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Tue, 1 Jun 2021 14:17:50 +0800 Subject: [PATCH 12/18] =?UTF-8?q?refactor(violin):=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=97=A0=E7=94=A8=E7=9A=84=E9=85=8D=E7=BD=AE=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plots/violin/constant.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/plots/violin/constant.ts b/src/plots/violin/constant.ts index c1f9cc6f0b..1308506273 100644 --- a/src/plots/violin/constant.ts +++ b/src/plots/violin/constant.ts @@ -24,20 +24,6 @@ export const DEFAULT_OPTIONS = deepAssign({}, Plot.getDefaultOptions(), { width: 3, }, - // 默认平滑 - smooth: true, - - // 默认显示箱线图 - box: { - textMap: { - max: 'Max', - min: 'Min', - q1: 'Q1', - q3: 'Q3', - median: 'Median', - }, - }, - // 默认小提琴轮廓样式 violinStyle: { lineWidth: 1, From e9bee389a81e4a8c247be2d18776407f04e86530 Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Tue, 1 Jun 2021 14:18:59 +0800 Subject: [PATCH 13/18] =?UTF-8?q?fix(violin):=20=E6=9A=82=E6=97=B6?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=8B=E5=B0=8F=E6=8F=90=E7=90=B4=E5=9B=BE?= =?UTF-8?q?=E7=9A=84=20legend=20=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plots/violin/adaptor.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/plots/violin/adaptor.ts b/src/plots/violin/adaptor.ts index 7c1fecf52b..a66f641cec 100644 --- a/src/plots/violin/adaptor.ts +++ b/src/plots/violin/adaptor.ts @@ -1,5 +1,5 @@ import { Geometry } from '@antv/g2'; -import { get, omit, each } from '@antv/util'; +import { get, set, omit, each } from '@antv/util'; import { Params } from '../../core/adaptor'; import { interaction, theme, tooltip, annotation as baseAnnotation } from '../../adaptor/common'; import { interval, point, violin } from '../../adaptor/geometries'; @@ -227,13 +227,20 @@ function axis(params: Params): Params { */ function legend(params: Params): Params { const { chart, options } = params; - const { legend, seriesField } = options; + const { legend, seriesField, shape } = options; if (legend === false) { chart.legend(false); } else { const legendField = seriesField ? seriesField : X_FIELD; - chart.legend(legendField, omit(legend as any, ['selected'])); + // fixme 暂不明为啥有描边 + const legendOptions = omit(legend as any, ['selected']); + if (!shape || !shape.startsWith('hollow')) { + if (!get(legendOptions, ['marker', 'style', 'lineWidth'])) { + set(legendOptions, ['marker', 'style', 'lineWidth'], 0); + } + } + chart.legend(legendField, legendOptions); // 特殊的处理 fixme G2 层得解决这个问题 if (get(legend, 'selected')) { each(chart.views, (view) => view.legend(legendField, legend)); From 755b1f5dcc0567cc61d23cb7d864e2bdc80b5716 Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Tue, 1 Jun 2021 14:25:03 +0800 Subject: [PATCH 14/18] =?UTF-8?q?docs:=20=E4=BC=98=E5=8C=96=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E6=96=87=E6=A1=A3=EF=BC=8C=E5=B0=8F=E6=8F=90=E7=90=B4?= =?UTF-8?q?=E5=9B=BE=E6=B2=A1=E6=9C=89=20slider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/plots/violin.en.md | 4 ++-- docs/api/plots/violin.zh.md | 2 +- docs/common/common-component.en.md | 23 +++++++++++++++++++++++ docs/common/common-component.zh.md | 23 +++++++++++++++++++++++ docs/common/component.en.md | 22 +--------------------- docs/common/component.zh.md | 22 +--------------------- 6 files changed, 51 insertions(+), 45 deletions(-) create mode 100644 docs/common/common-component.en.md create mode 100644 docs/common/common-component.zh.md diff --git a/docs/api/plots/violin.en.md b/docs/api/plots/violin.en.md index 32961c3e1f..0314f30004 100644 --- a/docs/api/plots/violin.en.md +++ b/docs/api/plots/violin.en.md @@ -1,5 +1,5 @@ --- -title: Box +title: Violin order: 31 --- @@ -81,7 +81,7 @@ Violin graphic style. ### Plot Components -`markdown:docs/common/component.en.md` +`markdown:docs/common/common-component.en.md` ### Plot Event diff --git a/docs/api/plots/violin.zh.md b/docs/api/plots/violin.zh.md index f9acef7808..bef35c45b8 100644 --- a/docs/api/plots/violin.zh.md +++ b/docs/api/plots/violin.zh.md @@ -85,7 +85,7 @@ type KdeOptions = { ### 图表组件 -`markdown:docs/common/component.zh.md` +`markdown:docs/common/common-component.zh.md` ### 图表事件 diff --git a/docs/common/common-component.en.md b/docs/common/common-component.en.md new file mode 100644 index 0000000000..1de93fd550 --- /dev/null +++ b/docs/common/common-component.en.md @@ -0,0 +1,23 @@ +#### axis + +xAxis、yAxis 配置相同。**注意**:由于 DualAxes(双轴图) 和 BidirectionalBar(对称条形图) 是双 y 轴, yAxis 类型是以 yField 中的字段作为 `key` 值的`object`。 + +`markdown:docs/common/axis.en.md` + +#### legend + +`markdown:docs/common/legend.en.md` + + +#### label + +`markdown:docs/common/label.en.md` + + +#### tooltip + +`markdown:docs/common/tooltip.en.md` + +#### annotations + +`markdown:docs/common/annotations.en.md` diff --git a/docs/common/common-component.zh.md b/docs/common/common-component.zh.md new file mode 100644 index 0000000000..6281c0f5e3 --- /dev/null +++ b/docs/common/common-component.zh.md @@ -0,0 +1,23 @@ +#### axis + +xAxis、yAxis 配置相同。**注意**:由于 DualAxes(双轴图) 和 BidirectionalBar(对称条形图) 是双 y 轴, yAxis 类型是以 yField 中的字段作为 `key` 值的`object`。 + +`markdown:docs/common/axis.zh.md` + +#### legend + +`markdown:docs/common/legend.zh.md` + + +#### label + +`markdown:docs/common/label.zh.md` + + +#### tooltip + +`markdown:docs/common/tooltip.zh.md` + +#### annotations + +`markdown:docs/common/annotations.zh.md` diff --git a/docs/common/component.en.md b/docs/common/component.en.md index ff86859164..78cd1c64b1 100644 --- a/docs/common/component.en.md +++ b/docs/common/component.en.md @@ -1,24 +1,4 @@ -#### tooltip - -`markdown:docs/common/tooltip.en.md` - -#### label - -`markdown:docs/common/label.en.md` - -#### axis - -Same for xAxis and yAxis. **Note**: Since `DualAxes` or `BidirectionalBar` has double Y-axes, the yAxis is a object which takes the field in yField as the 'key'. - -`markdown:docs/common/axis.en.md` - -#### legend - -`markdown:docs/common/legend.en.md` - -#### annotations - -`markdown:docs/common/annotations.en.md` +`markdown:docs/common/common-component.en.md` #### slider diff --git a/docs/common/component.zh.md b/docs/common/component.zh.md index b215cfb4bc..d9606b9291 100644 --- a/docs/common/component.zh.md +++ b/docs/common/component.zh.md @@ -1,24 +1,4 @@ -#### tooltip - -`markdown:docs/common/tooltip.zh.md` - -#### label - -`markdown:docs/common/label.zh.md` - -#### axis - -xAxis、yAxis 配置相同。**注意**:由于 DualAxes(双轴图) 和 BidirectionalBar(对称条形图) 是双 y 轴, yAxis 类型是以 yField 中的字段作为 `key` 值的`object`。 - -`markdown:docs/common/axis.zh.md` - -#### legend - -`markdown:docs/common/legend.zh.md` - -#### annotations - -`markdown:docs/common/annotations.zh.md` +`markdown:docs/common/common-component.zh.md` #### slider From 7bf05334a1331ebd66923f7b97804cd52246a607 Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Tue, 1 Jun 2021 14:28:20 +0800 Subject: [PATCH 15/18] =?UTF-8?q?chore:=20=E5=8D=87=E7=BA=A7=20surge-preiv?= =?UTF-8?q?ew?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/auto-inspection.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-inspection.yml b/.github/workflows/auto-inspection.yml index 814433b158..797498937e 100644 --- a/.github/workflows/auto-inspection.yml +++ b/.github/workflows/auto-inspection.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v2 - name: Get branch name (pull request) run: echo "BRANCH_NAME=$(echo ${GITHUB_HEAD_REF})" >> $GITHUB_ENV - - uses: lxfu1/surge-preview@v2.0.1 + - uses: lxfu1/surge-preview@v2.0.1 id: preview_step with: project_name: 'G2Plot' From 40275262ea6cfd03a3aa67193a3008597b40d9d6 Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Tue, 1 Jun 2021 14:35:51 +0800 Subject: [PATCH 16/18] =?UTF-8?q?docs:=20=E5=B0=8F=E6=8F=90=E7=90=B4?= =?UTF-8?q?=E5=9B=BE=E6=9A=82=E6=97=B6=E4=B8=8D=E6=94=AF=E6=8C=81=20label?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/common/common-component.en.md | 2 ++ docs/common/common-component.zh.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/common/common-component.en.md b/docs/common/common-component.en.md index 1de93fd550..54c047dfcf 100644 --- a/docs/common/common-component.en.md +++ b/docs/common/common-component.en.md @@ -11,6 +11,8 @@ xAxis、yAxis 配置相同。**注意**:由于 DualAxes(双轴图) 和 Bidirec #### label +> Label configuration is not supported in violin,you can use `annnotation` to replace it. + `markdown:docs/common/label.en.md` diff --git a/docs/common/common-component.zh.md b/docs/common/common-component.zh.md index 6281c0f5e3..c19f15a3b3 100644 --- a/docs/common/common-component.zh.md +++ b/docs/common/common-component.zh.md @@ -11,6 +11,8 @@ xAxis、yAxis 配置相同。**注意**:由于 DualAxes(双轴图) 和 Bidirec #### label +> 小提琴图暂时不支持 label 展示,可以使用 annnotation 进行替代 + `markdown:docs/common/label.zh.md` From 682ed93c4ad9d2a207d1bd62f20740e58b490172 Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Wed, 2 Jun 2021 09:13:26 +0800 Subject: [PATCH 17/18] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20violin=20?= =?UTF-8?q?=E5=8D=95=E6=B5=8B=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/unit/plots/violin/legend-spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/__tests__/unit/plots/violin/legend-spec.ts b/__tests__/unit/plots/violin/legend-spec.ts index 569fc64b66..3455d4ccdb 100644 --- a/__tests__/unit/plots/violin/legend-spec.ts +++ b/__tests__/unit/plots/violin/legend-spec.ts @@ -136,6 +136,9 @@ describe('violin legend', () => { violin.update({ legend: { position: 'left', marker: { style: { fill: 'red' } } } }); // @ts-ignore - expect(violin.chart.options.legends[X_FIELD]).toEqual({ position: 'left', marker: { style: { fill: 'red' } } }); + expect(violin.chart.options.legends[X_FIELD]).toMatchObject({ + position: 'left', + marker: { style: { fill: 'red' } }, + }); }); }); From b5560fbc3224333bc6bfb433aa8b99abcdabd0e5 Mon Sep 17 00:00:00 2001 From: visiky <736929286@qq.com> Date: Fri, 4 Jun 2021 16:08:29 +0800 Subject: [PATCH 18/18] =?UTF-8?q?feat(violin):=20=E5=B0=8F=E6=8F=90?= =?UTF-8?q?=E7=90=B4=E5=9B=BE=E5=A2=9E=E5=8A=A0=20box=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/unit/plots/violin/box-spec.ts | 4 ++-- docs/api/plots/violin.en.md | 6 ++++++ docs/api/plots/violin.zh.md | 6 ++++++ src/plots/violin/adaptor.ts | 6 +++--- src/plots/violin/types.ts | 2 ++ 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/__tests__/unit/plots/violin/box-spec.ts b/__tests__/unit/plots/violin/box-spec.ts index ec049e4c3c..67d0b1df42 100644 --- a/__tests__/unit/plots/violin/box-spec.ts +++ b/__tests__/unit/plots/violin/box-spec.ts @@ -26,14 +26,14 @@ describe('violin', () => { }); // 暂时不开放 box 配置 - it.skip("should not render box views when 'box' set to false.", () => { + it("should not render box views when 'box' set to false.", () => { const violin = new Violin(createDiv(), { width: 400, height: 500, data: BASE_VIOLIN_DATA, xField: 'type', yField: 'value', - // box: false, + box: false, }); violin.render(); diff --git a/docs/api/plots/violin.en.md b/docs/api/plots/violin.en.md index 0314f30004..c5da807e6b 100644 --- a/docs/api/plots/violin.en.md +++ b/docs/api/plots/violin.en.md @@ -63,6 +63,12 @@ type KdeOptions = { ### Graphic Style +#### box + +**optional** _boolean_ + +Whether to show box plot. Default show box plot, you could hide box plot by setting `box: false`. + #### shape **optional** _'smooth'|'hollow'|'hollow-smooth'_ diff --git a/docs/api/plots/violin.zh.md b/docs/api/plots/violin.zh.md index bef35c45b8..9a87438af6 100644 --- a/docs/api/plots/violin.zh.md +++ b/docs/api/plots/violin.zh.md @@ -65,6 +65,12 @@ type KdeOptions = { ### 图形样式 +#### box + +**optional** _boolean_ + +是否展示内部箱线图。默认展示,设置为 'false' 关闭箱线图。 + #### shape **optional** _'smooth'|'hollow'|'hollow-smooth'_ diff --git a/src/plots/violin/adaptor.ts b/src/plots/violin/adaptor.ts index a66f641cec..1e6144c2ea 100644 --- a/src/plots/violin/adaptor.ts +++ b/src/plots/violin/adaptor.ts @@ -68,10 +68,10 @@ function violinView(params: Params): Params { /** 箱线 */ function boxView(params: Params): Params { const { chart, options } = params; - const { seriesField, color, tooltip } = options; + const { seriesField, color, tooltip, box } = options; - // 如果配置 `box` 为 false ,不渲染内部箱线图 (暂时不开放 关闭) - // if (!box) return params; + // 如果配置 `box` 为 false ,不渲染内部箱线图 + if (box === false) return params; // 边缘线 const minMaxView = chart.createView({ id: MIN_MAX_VIEW_ID }); diff --git a/src/plots/violin/types.ts b/src/plots/violin/types.ts index b413d83e7b..8665a14ab6 100644 --- a/src/plots/violin/types.ts +++ b/src/plots/violin/types.ts @@ -7,6 +7,8 @@ export interface ViolinOptions extends Options { readonly yField: string; /** 拆分字段映射,默认是分组情况,颜色作为视觉通道 */ readonly seriesField?: string; + /** 内部箱线图配置,false 为不显示。 */ + readonly box?: boolean; /** * 小提琴的形状。 * 默认: 非平滑、实心