From 05afca6e32eb8b72eb21fa5dc93c5abb54b10580 Mon Sep 17 00:00:00 2001 From: Kasmine <736929286@qq.com> Date: Mon, 10 Aug 2020 14:41:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=A5=BC=E3=80=81=E7=8E=AF?= =?UTF-8?q?=E5=9B=BE=E6=96=87=E6=A1=A3=20=EF=BC=8C=E5=90=8C=E6=97=B6?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=E9=A5=BC=E5=9B=BE=E8=83=BD=E5=8A=9B=20(#1411?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(v2/core): 增加 default cfg 配置 * feat(v2/legend-adaptor): 增加通用 legend adaptor, 修复 饼图 legend 关闭失效 * feat(v2/pie-inner-label): 增加 pie-inner label 类型 & 增 强 statistic statistic 增加title 和 content boolean 类型,允许显式关闭 * test: 增加测试用例 * docs(v2/pie): 增加饼/环 demo & api 文档 - 移除 radius 默认配置 --- __tests__/unit/plots/pie/label-spec.ts | 7 + __tests__/unit/plots/pie/legend-spec.ts | 31 ++++ __tests__/unit/plots/pie/utils-spec.ts | 12 +- __tests__/utils/event.ts | 2 +- examples/pie/basic/API.en.md | 186 ++++++++++++++++++++++++ examples/pie/basic/API.zh.md | 186 ++++++++++++++++++++++++ examples/pie/basic/demo/basic.ts | 30 ++++ examples/pie/basic/demo/custom-label.ts | 49 +++++++ examples/pie/basic/demo/meta.json | 48 ++++++ examples/pie/basic/demo/outer-label.ts | 25 ++++ examples/pie/basic/demo/pie-texture.ts | 36 +++++ examples/pie/basic/index.en.md | 4 + examples/pie/basic/index.zh.md | 4 + examples/pie/donut/API.en.md | 50 +++++++ examples/pie/donut/API.zh.md | 50 +++++++ examples/pie/donut/demo/basic.ts | 37 +++++ examples/pie/donut/demo/meta.json | 24 +++ examples/pie/donut/demo/statistics.ts | 34 +++++ examples/pie/donut/index.en.md | 4 + examples/pie/donut/index.zh.md | 4 + gatsby-browser.js | 1 + gatsby-config.js | 8 + src/adaptor/common.ts | 17 +++ src/core/plot.ts | 12 +- src/plots/pie/adaptor.ts | 94 +++++++----- src/plots/pie/index.ts | 13 ++ src/plots/pie/label/index.ts | 4 + src/plots/pie/label/inner-label.ts | 37 +++++ src/plots/pie/types.ts | 3 + src/plots/pie/utils.ts | 15 +- 30 files changed, 984 insertions(+), 43 deletions(-) create mode 100644 __tests__/unit/plots/pie/legend-spec.ts create mode 100644 examples/pie/basic/API.en.md create mode 100644 examples/pie/basic/API.zh.md create mode 100644 examples/pie/basic/demo/basic.ts create mode 100644 examples/pie/basic/demo/custom-label.ts create mode 100644 examples/pie/basic/demo/meta.json create mode 100644 examples/pie/basic/demo/outer-label.ts create mode 100644 examples/pie/basic/demo/pie-texture.ts create mode 100644 examples/pie/basic/index.en.md create mode 100644 examples/pie/basic/index.zh.md create mode 100644 examples/pie/donut/API.en.md create mode 100644 examples/pie/donut/API.zh.md create mode 100644 examples/pie/donut/demo/basic.ts create mode 100644 examples/pie/donut/demo/meta.json create mode 100644 examples/pie/donut/demo/statistics.ts create mode 100644 examples/pie/donut/index.en.md create mode 100644 examples/pie/donut/index.zh.md create mode 100644 src/plots/pie/label/index.ts create mode 100644 src/plots/pie/label/inner-label.ts diff --git a/__tests__/unit/plots/pie/label-spec.ts b/__tests__/unit/plots/pie/label-spec.ts index 4033c3bc4d4..4803cc59b5e 100644 --- a/__tests__/unit/plots/pie/label-spec.ts +++ b/__tests__/unit/plots/pie/label-spec.ts @@ -1,4 +1,5 @@ import { IGroup } from '@antv/g-base'; +import { getGeometryLabel } from '@antv/g2'; import { Pie } from '../../../../src'; import { POSITIVE_NEGATIVE_DATA } from '../../../data/common'; import { createDiv } from '../../../utils/dom'; @@ -133,3 +134,9 @@ describe('support template string formatter', () => { expect((labels[0] as IGroup).getChildren()[0].attr('text')).toBe('item1: 1(20.00%)'); // todo 补充图例点击后,百分比计算依然准确的 case }); + +describe('inner label', () => { + it('自定义注册饼图 inner label', () => { + expect(getGeometryLabel('pie-inner')).toBeDefined(); + }); +}); diff --git a/__tests__/unit/plots/pie/legend-spec.ts b/__tests__/unit/plots/pie/legend-spec.ts new file mode 100644 index 00000000000..ef27358c1cf --- /dev/null +++ b/__tests__/unit/plots/pie/legend-spec.ts @@ -0,0 +1,31 @@ +import { Pie } from '../../../../src'; +import { createDiv } from '../../../utils/dom'; + +describe('pie legend', () => { + const pie = new Pie(createDiv(), { + width: 400, + height: 400, + data: [ + { type: '分类一', value: 10 }, + { type: '分类二', value: 20 }, + { type: '分类三', value: 15 }, + { type: '其他', value: 23 }, + ], + angleField: 'value', + colorField: 'type', + }); + + pie.render(); + + it('移除 legend', () => { + const legendController = pie.chart.getController('legend'); + const legendComponent = legendController.getComponents()[0].component; + expect(legendComponent.get('items').length).toBe(4); + + pie.update({ + ...pie.options, + legend: false, + }); + expect(legendComponent.get('items')).toBeUndefined(); + }); +}); diff --git a/__tests__/unit/plots/pie/utils-spec.ts b/__tests__/unit/plots/pie/utils-spec.ts index 7fcaa5bd3cc..5655a514d7c 100644 --- a/__tests__/unit/plots/pie/utils-spec.ts +++ b/__tests__/unit/plots/pie/utils-spec.ts @@ -1,6 +1,6 @@ import { Pie } from '../../../../src'; -import { getStatisticData, getTotalValue } from '../../../../src/plots/pie/utils'; import { createDiv } from '../../../utils/dom'; +import { getStatisticData, getTotalValue, parsePercentageToNumber } from '../../../../src/plots/pie/utils'; describe('utils of pie plot', () => { const data = [ @@ -87,4 +87,14 @@ describe('utils of pie plot', () => { value: '20', }); }); + + it('将 字符串百分比 转换为 数值型百分比', () => { + // @ts-ignore 不合法的入参 + expect(parsePercentageToNumber(null)).toBe(null); + // @ts-ignore 不合法的入参 + expect(parsePercentageToNumber(100)).toBe(100); + expect(parsePercentageToNumber('0.35')).toBe(0.35); + expect(parsePercentageToNumber('34%')).toBe(0.34); + expect(parsePercentageToNumber('0%')).toBe(0); + }); }); diff --git a/__tests__/utils/event.ts b/__tests__/utils/event.ts index 9f4dd477062..4f9e9cb35ae 100644 --- a/__tests__/utils/event.ts +++ b/__tests__/utils/event.ts @@ -1,4 +1,4 @@ -import { IElement } from '../../src/dependents'; +import { IElement } from '@antv/g2/lib/dependents'; // 触发 Canvas 上元素的鼠标事件 export const simulateMouseEvent = (element: IElement, event: string, cfg = {}) => { diff --git a/examples/pie/basic/API.en.md b/examples/pie/basic/API.en.md new file mode 100644 index 00000000000..8ebde744709 --- /dev/null +++ b/examples/pie/basic/API.en.md @@ -0,0 +1,186 @@ +--- +title: API +--- + +# 配置属性 + +## 图表容器 + +- 见 [通用配置](TODO) + +## 数据映射 + +### data 📌 + +**必选**, _array object_ + +功能描述: 设置图表数据源 + +默认配置: 无 + +数据源为对象集合,例如:`[{ time: '1991',value: 20 }, { time: '1992',value: 20 }]`。 + +### meta + +**可选**, _object_ + +功能描述: 全局化配置图表数据元信息,以字段为单位进行配置。在 meta 上的配置将同时影响所有组件的文本信息。 + +默认配置: 无 + +| 细分配置项名称 | 类型 | 功能描述 | +| -------------- | ---------- | ------------------------------------------- | +| alias | _string_ | 字段的别名 | +| formatter | _function_ | callback 方法,对该字段所有值进行格式化处理 | +| values | _string[]_ | 枚举该字段下所有值 | +| range | _number[]_ | 字段的数据映射区间,默认为[0,1] | + +```js +const data = [ + { country: 'Asia', year: '1750', value: 502,}, + { country: 'Asia', year: '1800', value: 635,}, + { country: 'Europe', year: '1750', value: 163,}, + { country: 'Europe', year: '1800', value: 203,}, +]; + +const piePlot = new Pie(document.getElementById('container'), { + data, + // highlight-start + meta: { + country: { + alias:'国家' + range: [0, 1], + }, + value: { + alias: '数量', + formatter:(v)=>{return `${v}个`} + } + }, + // highlight-end + angleField: 'value', + colorField: 'country', +}); +piePlot.render(); +``` + +### angleField 📌 + +**必选**, _string_ + +功能描述: 扇形切片大小(弧度)所对应的数据字段名。。 + +默认配置: 无 + +### colorField 📌 + +**可选**, _string_ + +功能描述: 扇形颜色映射对应的数据字段名。 + +默认配置: 无 + +## 图形样式 + +### radius ✨ + +**可选**, _number_ + +功能描述: 饼图的半径,原点为画布中心。配置值域为 [0,1],0 代表饼图大小为 0,即不显示,1 代表饼图撑满绘图区域。 + +### color + +**可选**, _string | string[] | Function_ + +功能描述: 指定扇形颜色,即可以指定一系列色值,也可以通过回调函数的方法根据对应数值进行设置。 + +默认配置:采用 theme 中的色板。 + +用法示例: + +```js +// 配合颜色映射,指定多值 +colorField:'type', +color:['blue','yellow','green'] +//配合颜色映射,使用回调函数指定色值 +colorField:'type', +color:(d)=>{ + if(d==='a') return 'red'; + return 'blue'; +} +``` + +### pieStyle ✨ + +**可选**, _object_ + +功能描述: 设置扇形样式。pieStyle 中的`fill`会覆盖 `color` 的配置。pieStyle 可以直接指定,也可以通过 callback 的方式,根据数据为每个扇形切片指定单独的样式。 + +默认配置: 无 + +| 细分配置 | 类型 | 功能描述 | +| ------------- | ------ | ---------- | +| fill | _string_ | 填充颜色 | +| stroke | _string_ | 描边颜色 | +| lineWidth | _number_ | 描边宽度 | +| lineDash | _number_ | 虚线描边 | +| opacity | _number_ | 整体透明度 | +| fillOpacity | _number_ | 填充透明度 | +| strokeOpacity | _number_ | 描边透明度 | + +## 图表组件 + + + +### legend、tooltip、theme + +`legend` 、`tooltip`、`theme` 等通用组件请参考图表通用配置 + +### label ✨ + +功能描述: 标签文本 + +[DEMO1](../../pie/basic#basic) +[DEMO2](../../pie/basic#outer-label) + +| 细分配置 | 类型 | 功能描述 | +| ------------- | ------ | ---------- | +| type | `inner`, `outer` | 标签类型 | +| content | _string_, _Fucntion_ | 标签内容,可通过回调的方式,也支持模板字符串配置:内置标签名(`{name}`)、百分比(`{percentage}`)、数值(`{value}`) 三种 | +| style | _object, _Fucntion_ | 标签样式,可通过回调的方式 | +| 其他 | any | 其他,请参考图表 label 通用配置 | + + +## 事件 + +[通用 events](../../general/events/API) + +# 图表方法 + +## render() 📌 + +**必选** + +渲染图表。 + +## update() + +**可选** + +更新图表配置项。 + +```js +piePlot.update({ + ...piePlot.options, + legend: false, +}); +``` + +## changeData() + +**可选** + +更新图表数据。`update()`方法会导致图形区域销毁并重建,如果只进行数据更新,而不涉及其他配置项更新,推荐使用本方法。 + +```js +piePlot.changeData(newData); +``` diff --git a/examples/pie/basic/API.zh.md b/examples/pie/basic/API.zh.md new file mode 100644 index 00000000000..8ebde744709 --- /dev/null +++ b/examples/pie/basic/API.zh.md @@ -0,0 +1,186 @@ +--- +title: API +--- + +# 配置属性 + +## 图表容器 + +- 见 [通用配置](TODO) + +## 数据映射 + +### data 📌 + +**必选**, _array object_ + +功能描述: 设置图表数据源 + +默认配置: 无 + +数据源为对象集合,例如:`[{ time: '1991',value: 20 }, { time: '1992',value: 20 }]`。 + +### meta + +**可选**, _object_ + +功能描述: 全局化配置图表数据元信息,以字段为单位进行配置。在 meta 上的配置将同时影响所有组件的文本信息。 + +默认配置: 无 + +| 细分配置项名称 | 类型 | 功能描述 | +| -------------- | ---------- | ------------------------------------------- | +| alias | _string_ | 字段的别名 | +| formatter | _function_ | callback 方法,对该字段所有值进行格式化处理 | +| values | _string[]_ | 枚举该字段下所有值 | +| range | _number[]_ | 字段的数据映射区间,默认为[0,1] | + +```js +const data = [ + { country: 'Asia', year: '1750', value: 502,}, + { country: 'Asia', year: '1800', value: 635,}, + { country: 'Europe', year: '1750', value: 163,}, + { country: 'Europe', year: '1800', value: 203,}, +]; + +const piePlot = new Pie(document.getElementById('container'), { + data, + // highlight-start + meta: { + country: { + alias:'国家' + range: [0, 1], + }, + value: { + alias: '数量', + formatter:(v)=>{return `${v}个`} + } + }, + // highlight-end + angleField: 'value', + colorField: 'country', +}); +piePlot.render(); +``` + +### angleField 📌 + +**必选**, _string_ + +功能描述: 扇形切片大小(弧度)所对应的数据字段名。。 + +默认配置: 无 + +### colorField 📌 + +**可选**, _string_ + +功能描述: 扇形颜色映射对应的数据字段名。 + +默认配置: 无 + +## 图形样式 + +### radius ✨ + +**可选**, _number_ + +功能描述: 饼图的半径,原点为画布中心。配置值域为 [0,1],0 代表饼图大小为 0,即不显示,1 代表饼图撑满绘图区域。 + +### color + +**可选**, _string | string[] | Function_ + +功能描述: 指定扇形颜色,即可以指定一系列色值,也可以通过回调函数的方法根据对应数值进行设置。 + +默认配置:采用 theme 中的色板。 + +用法示例: + +```js +// 配合颜色映射,指定多值 +colorField:'type', +color:['blue','yellow','green'] +//配合颜色映射,使用回调函数指定色值 +colorField:'type', +color:(d)=>{ + if(d==='a') return 'red'; + return 'blue'; +} +``` + +### pieStyle ✨ + +**可选**, _object_ + +功能描述: 设置扇形样式。pieStyle 中的`fill`会覆盖 `color` 的配置。pieStyle 可以直接指定,也可以通过 callback 的方式,根据数据为每个扇形切片指定单独的样式。 + +默认配置: 无 + +| 细分配置 | 类型 | 功能描述 | +| ------------- | ------ | ---------- | +| fill | _string_ | 填充颜色 | +| stroke | _string_ | 描边颜色 | +| lineWidth | _number_ | 描边宽度 | +| lineDash | _number_ | 虚线描边 | +| opacity | _number_ | 整体透明度 | +| fillOpacity | _number_ | 填充透明度 | +| strokeOpacity | _number_ | 描边透明度 | + +## 图表组件 + + + +### legend、tooltip、theme + +`legend` 、`tooltip`、`theme` 等通用组件请参考图表通用配置 + +### label ✨ + +功能描述: 标签文本 + +[DEMO1](../../pie/basic#basic) +[DEMO2](../../pie/basic#outer-label) + +| 细分配置 | 类型 | 功能描述 | +| ------------- | ------ | ---------- | +| type | `inner`, `outer` | 标签类型 | +| content | _string_, _Fucntion_ | 标签内容,可通过回调的方式,也支持模板字符串配置:内置标签名(`{name}`)、百分比(`{percentage}`)、数值(`{value}`) 三种 | +| style | _object, _Fucntion_ | 标签样式,可通过回调的方式 | +| 其他 | any | 其他,请参考图表 label 通用配置 | + + +## 事件 + +[通用 events](../../general/events/API) + +# 图表方法 + +## render() 📌 + +**必选** + +渲染图表。 + +## update() + +**可选** + +更新图表配置项。 + +```js +piePlot.update({ + ...piePlot.options, + legend: false, +}); +``` + +## changeData() + +**可选** + +更新图表数据。`update()`方法会导致图形区域销毁并重建,如果只进行数据更新,而不涉及其他配置项更新,推荐使用本方法。 + +```js +piePlot.changeData(newData); +``` diff --git a/examples/pie/basic/demo/basic.ts b/examples/pie/basic/demo/basic.ts new file mode 100644 index 00000000000..5907b5fb98f --- /dev/null +++ b/examples/pie/basic/demo/basic.ts @@ -0,0 +1,30 @@ +import { Pie } from '@antv/g2plot'; + +const data = [ + { type: '分类一', value: 27 }, + { type: '分类二', value: 25 }, + { type: '分类三', value: 18 }, + { type: '分类四', value: 15 }, + { type: '分类五', value: 10 }, + { type: '其他', value: 5 }, +]; + +const piePlot = new Pie(document.getElementById('container'), { + width: 400, + height: 300, + appendPadding: 10, + data, + angleField: 'value', + colorField: 'type', + radius: 0.8, + label: { + type: 'inner', + content: '{name} {percentage}', + style: { + fill: '#fff', + fontSize: 14, + }, + }, +}); + +piePlot.render(); diff --git a/examples/pie/basic/demo/custom-label.ts b/examples/pie/basic/demo/custom-label.ts new file mode 100644 index 00000000000..042212a7b55 --- /dev/null +++ b/examples/pie/basic/demo/custom-label.ts @@ -0,0 +1,49 @@ +import { Pie } from '@antv/g2plot'; + +const data = [ + { sex: '男', sold: 0.45 }, + { sex: '女', sold: 0.55 }, +]; + +const piePlot = new Pie(document.getElementById('container'), { + width: 400, + height: 300, + appendPadding: 10, + data, + angleField: 'sold', + colorField: 'sex', + radius: 0.8, + label: { + content: (obj) => { + const group = new (window as any).G.Group({}); + group.addShape({ + type: 'image', + attrs: { + x: 0, + y: 0, + width: 40, + height: 50, + img: + obj.sex === '男' + ? 'https://gw.alipayobjects.com/zos/rmsportal/oeCxrAewtedMBYOETCln.png' + : 'https://gw.alipayobjects.com/zos/rmsportal/mweUsJpBWucJRixSfWVP.png', + }, + }); + + group.addShape({ + type: 'text', + attrs: { + x: 20, + y: 54, + text: obj.sex, + textAlign: 'center', + textBaseline: 'top', + fill: obj.sex === '男' ? '#1890ff' : '#f04864', + }, + }); + return group; + }, + }, +}); + +piePlot.render(); diff --git a/examples/pie/basic/demo/meta.json b/examples/pie/basic/demo/meta.json new file mode 100644 index 00000000000..f4c2953cfad --- /dev/null +++ b/examples/pie/basic/demo/meta.json @@ -0,0 +1,48 @@ +{ + "title": { + "zh": "中文分类", + "en": "Category" + }, + "demos": [ + { + "filename": "basic.ts", + "title": { + "zh": "饼图", + "en": "basic Pie chart" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*wmldRZZj9lIAAAAAAAAAAABkARQnAQ" + }, + { + "filename": "outer-label.ts", + "title": { + "zh": "饼图-外部图形标签", + "en": "Pie chart - outter label" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*ZztJQa4RLwoAAAAAAAAAAABkARQnAQ" + }, + { + "filename": "spider-label.ts", + "title": { + "zh": "饼图-图形标签蜘蛛布局", + "en": "Pie chart - spider-layout label" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*ahB1Qp7T-C8AAAAAAAAAAABkARQnAQ" + }, + { + "filename": "custom-label.ts", + "title": { + "en": "customize pie chart", + "zh": "个性化标签饼图" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_f5c722/afts/img/A*sqwJRIEzdQ0AAAAAAAAAAABkARQnAQ" + }, + { + "filename": "pie-texture.ts", + "title": { + "en": "pie chart fill with texture", + "zh": "饼图-带纹理" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_2274c3/afts/img/A*6DhLR77aZloAAAAAAAAAAABkARQnAQ" + } + ] +} diff --git a/examples/pie/basic/demo/outer-label.ts b/examples/pie/basic/demo/outer-label.ts new file mode 100644 index 00000000000..a53450383ba --- /dev/null +++ b/examples/pie/basic/demo/outer-label.ts @@ -0,0 +1,25 @@ +import { Pie } from '@antv/g2plot'; + +const data = [ + { type: '分类一', value: 27 }, + { type: '分类二', value: 25 }, + { type: '分类三', value: 18 }, + { type: '分类四', value: 15 }, + { type: '分类五', value: 10 }, + { type: '其他', value: 5 }, +]; + +const piePlot = new Pie(document.getElementById('container'), { + width: 400, + height: 300, + appendPadding: 10, + data, + angleField: 'value', + colorField: 'type', + radius: 0.8, + label: { + type: 'outer', + }, +}); + +piePlot.render(); diff --git a/examples/pie/basic/demo/pie-texture.ts b/examples/pie/basic/demo/pie-texture.ts new file mode 100644 index 00000000000..f40c5f2b1c9 --- /dev/null +++ b/examples/pie/basic/demo/pie-texture.ts @@ -0,0 +1,36 @@ +import { Pie } from '@antv/g2plot'; + +const data = [ + { sex: '男', sold: 0.45 }, + { sex: '女', sold: 0.55 }, +]; + +const piePlot = new Pie(document.getElementById('container'), { + width: 400, + height: 300, + appendPadding: 10, + data, + angleField: 'sold', + colorField: 'sex', + radius: 0.8, + legend: false, + label: { + type: 'inner', + style: { + fill: '#fff', + fontSize: 18, + }, + }, + pieStyle: (solid, sex) => { + if (sex === '男') { + return { + fill: 'p(a)https://gw.alipayobjects.com/zos/rmsportal/nASTPWDPJDMgkDRlAUmw.jpeg', + }; + } + return { + fill: 'p(a)https://gw.alipayobjects.com/zos/rmsportal/ziMWHpHSTlTzURSzCarw.jpeg', + }; + }, +}); + +piePlot.render(); diff --git a/examples/pie/basic/index.en.md b/examples/pie/basic/index.en.md new file mode 100644 index 00000000000..9eca165af1d --- /dev/null +++ b/examples/pie/basic/index.en.md @@ -0,0 +1,4 @@ +--- +title: Pie +order: 0 +--- diff --git a/examples/pie/basic/index.zh.md b/examples/pie/basic/index.zh.md new file mode 100644 index 00000000000..393ef30d2fd --- /dev/null +++ b/examples/pie/basic/index.zh.md @@ -0,0 +1,4 @@ +--- +title: 饼图 +order: 0 +--- diff --git a/examples/pie/donut/API.en.md b/examples/pie/donut/API.en.md new file mode 100644 index 00000000000..7ac59f33bad --- /dev/null +++ b/examples/pie/donut/API.en.md @@ -0,0 +1,50 @@ +--- +title: API +--- + +# 配置属性 + +## 图表容器 + +- 见 [通用配置](TODO) + +## 基础配置 + +- 见 [饼图配置](TODO) + +## 特殊配置 + +### innerRadius ✨ + +**可选**, _number_ + +功能描述: 环图的内半径,原点为画布中心,若不配置内半径,则直接为饼图 + +### statistic ✨ + +**可选**, _object_ + +功能描述: 中心统计指标卡。当 `innerRadius` 配置不为 0 时,生效 + +用法示例: + +```js +{ + statistic: { + title: { + formatter: () => 'Total', + }, + }, + // 同时 可添加 中心统计文本 交互 + interactions: [{ name: 'pie-statistic-active' }] +} +``` + +| 细分配置 | 类型 | 功能描述 | +| ------------- | ------ | ---------- | +| title | _boolean_, _object_ | 指标卡标题 | +|     formatter | _function_ | 通过回调的方式,设置指标卡标题 | +|     style | _object_ | 指标卡标题样式 | +| content | _boolean_, _object_ | 指标卡内容 | +|     formatter | _function_ | 通过回调的方式,设置指标卡内容 | +|     style | _object_ | 指标卡内容样式 | \ No newline at end of file diff --git a/examples/pie/donut/API.zh.md b/examples/pie/donut/API.zh.md new file mode 100644 index 00000000000..7ac59f33bad --- /dev/null +++ b/examples/pie/donut/API.zh.md @@ -0,0 +1,50 @@ +--- +title: API +--- + +# 配置属性 + +## 图表容器 + +- 见 [通用配置](TODO) + +## 基础配置 + +- 见 [饼图配置](TODO) + +## 特殊配置 + +### innerRadius ✨ + +**可选**, _number_ + +功能描述: 环图的内半径,原点为画布中心,若不配置内半径,则直接为饼图 + +### statistic ✨ + +**可选**, _object_ + +功能描述: 中心统计指标卡。当 `innerRadius` 配置不为 0 时,生效 + +用法示例: + +```js +{ + statistic: { + title: { + formatter: () => 'Total', + }, + }, + // 同时 可添加 中心统计文本 交互 + interactions: [{ name: 'pie-statistic-active' }] +} +``` + +| 细分配置 | 类型 | 功能描述 | +| ------------- | ------ | ---------- | +| title | _boolean_, _object_ | 指标卡标题 | +|     formatter | _function_ | 通过回调的方式,设置指标卡标题 | +|     style | _object_ | 指标卡标题样式 | +| content | _boolean_, _object_ | 指标卡内容 | +|     formatter | _function_ | 通过回调的方式,设置指标卡内容 | +|     style | _object_ | 指标卡内容样式 | \ No newline at end of file diff --git a/examples/pie/donut/demo/basic.ts b/examples/pie/donut/demo/basic.ts new file mode 100644 index 00000000000..daa17185cc6 --- /dev/null +++ b/examples/pie/donut/demo/basic.ts @@ -0,0 +1,37 @@ +import { Pie } from '@antv/g2plot'; + +const data = [ + { type: '分类一', value: 27 }, + { type: '分类二', value: 25 }, + { type: '分类三', value: 18 }, + { type: '分类四', value: 15 }, + { type: '分类五', value: 10 }, + { type: '其他', value: 5 }, +]; + +const piePlot = new Pie(document.getElementById('container'), { + width: 400, + height: 300, + appendPadding: 10, + data, + angleField: 'value', + colorField: 'type', + radius: 0.8, + innerRadius: 0.6, + label: { + type: 'inner', + content: '{percentage}', + style: { + fill: '#fff', + fontSize: 14, + }, + }, + statistic: { + title: false, + content: { + formatter: () => 'AntV\nG2Plot', + }, + }, +}); + +piePlot.render(); diff --git a/examples/pie/donut/demo/meta.json b/examples/pie/donut/demo/meta.json new file mode 100644 index 00000000000..3bbb0294530 --- /dev/null +++ b/examples/pie/donut/demo/meta.json @@ -0,0 +1,24 @@ +{ + "title": { + "zh": "中文分类", + "en": "Category" + }, + "demos": [ + { + "filename": "basic.js", + "title": { + "zh": "环图", + "en": "basic Donut chart" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*wmldRZZj9lIAAAAAAAAAAABkARQnAQ" + }, + { + "filename": "statistics.js", + "title": { + "zh": "环图 - 带统计指标卡", + "en": "Donut chart with statistics" + }, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*ZztJQa4RLwoAAAAAAAAAAABkARQnAQ" + } + ] +} diff --git a/examples/pie/donut/demo/statistics.ts b/examples/pie/donut/demo/statistics.ts new file mode 100644 index 00000000000..75491427c98 --- /dev/null +++ b/examples/pie/donut/demo/statistics.ts @@ -0,0 +1,34 @@ +import { Pie } from '@antv/g2plot'; + +const data = [ + { type: '分类一', value: 27 }, + { type: '分类二', value: 25 }, + { type: '分类三', value: 18 }, + { type: '分类四', value: 15 }, + { type: '分类五', value: 10 }, + { type: '其他', value: 5 }, +]; + +const piePlot = new Pie(document.getElementById('container'), { + width: 400, + height: 300, + appendPadding: 10, + data, + angleField: 'value', + colorField: 'type', + radius: 0.8, + innerRadius: 0.64, + label: { + type: 'outer', + content: '{name} {percentage}', + }, + statistic: { + title: { + formatter: () => 'Total', + }, + }, + // 添加 中心统计文本 交互 + interactions: [{ name: 'pie-statistic-active' }], +}); + +piePlot.render(); diff --git a/examples/pie/donut/index.en.md b/examples/pie/donut/index.en.md new file mode 100644 index 00000000000..eb6c115894b --- /dev/null +++ b/examples/pie/donut/index.en.md @@ -0,0 +1,4 @@ +--- +title: Donut +order: 1 +--- diff --git a/examples/pie/donut/index.zh.md b/examples/pie/donut/index.zh.md new file mode 100644 index 00000000000..2b9cc813a78 --- /dev/null +++ b/examples/pie/donut/index.zh.md @@ -0,0 +1,4 @@ +--- +title: 环图 +order: 1 +--- diff --git a/gatsby-browser.js b/gatsby-browser.js index 355cbd4d295..234e6111f51 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -1,3 +1,4 @@ window.g2plot = require('./src/index.ts'); window.dataSet = require('@antv/data-set'); window.util = require('@antv/util'); +window.G = require('@antv/g-canvas'); \ No newline at end of file diff --git a/gatsby-config.js b/gatsby-config.js index d1407ca4c4d..c85c0d74855 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -47,6 +47,14 @@ module.exports = { ], docs: [], examples: [ + { + slug: 'pie', + icon: 'pie', + title: { + zh: '饼图', + en: 'Pie Charts', + }, + }, { slug: 'scatter', icon: 'point', diff --git a/src/adaptor/common.ts b/src/adaptor/common.ts index 319caf6f02f..217793778ba 100644 --- a/src/adaptor/common.ts +++ b/src/adaptor/common.ts @@ -7,6 +7,23 @@ import { Params } from '../core/adaptor'; import { Options } from '../types'; import { Interaction } from '../types/interaction'; +/** + * 通用 legend 配置, 适用于带 colorField 的图表 + * @param params + */ +export function legend(params: Params): Params { + const { chart, options } = params; + const { legend, colorField } = options; + + if (legend === false) { + chart.legend(false); + } else if (colorField) { + chart.legend(colorField, legend); + } + + return params; +} + /** * 通用 tooltip 配置 * @param params diff --git a/src/core/plot.ts b/src/core/plot.ts index ced92d2271f..1fc4ce589d7 100644 --- a/src/core/plot.ts +++ b/src/core/plot.ts @@ -1,4 +1,5 @@ import { Chart } from '@antv/g2'; +import { deepMix } from '@antv/util'; import { bind } from 'size-sensor'; import { Adaptor } from './adaptor'; import { ChartOptions, Data } from '../types'; @@ -20,7 +21,8 @@ export abstract class Plot { constructor(container: string | HTMLElement, options: O) { this.container = typeof container === 'string' ? document.getElementById(container) : container; - this.options = options; + const defaultOptions = this.getDefaultOptions(); + this.options = deepMix({}, defaultOptions, options); this.createG2(); } @@ -44,6 +46,14 @@ export abstract class Plot { }); } + /** + * 获取默认的 options 配置项 + * 每个组件都可以复写 + */ + protected getDefaultOptions(): Partial { + return {}; + } + /** * 每个组件有自己的 schema adaptor */ diff --git a/src/plots/pie/adaptor.ts b/src/plots/pie/adaptor.ts index 73d70e98b9d..e93c6904c5e 100644 --- a/src/plots/pie/adaptor.ts +++ b/src/plots/pie/adaptor.ts @@ -1,6 +1,6 @@ import { deepMix, each, every, filter, get, isFunction, isString, isNil } from '@antv/util'; import { Params } from '../../core/adaptor'; -import { tooltip, interaction, animation, theme } from '../../adaptor/common'; +import { legend, tooltip, interaction, animation, theme } from '../../adaptor/common'; import { flow, LEVEL, log, template } from '../../utils'; import { StatisticContentStyle, StatisticTitleStyle } from './constants'; import { PieOptions } from './types'; @@ -76,21 +76,6 @@ function coord(params: Params): Params { return params; } -/** - * legend 配置 - * @param params - */ -function legend(params: Params): Params { - const { chart, options } = params; - const { legend, colorField } = options; - - if (legend && colorField) { - chart.legend(colorField, legend); - } - - return params; -} - /** * label 配置 * @param params @@ -106,8 +91,10 @@ function label(params: Params): Params { } else { const { callback, ...cfg } = label; const labelCfg = cfg; - if (cfg.content) { - const { content } = cfg; + + // ① 提供模板字符串的 label content 配置 + if (labelCfg.content) { + const { content } = labelCfg; labelCfg.content = (data: object, dataum: any, index: number) => { const name = data[colorField]; const value = data[angleField]; @@ -127,8 +114,24 @@ function label(params: Params): Params { : content; }; } + + // ② 转换 label type 和 layout type + const LABEL_TYPE_MAP = { + inner: 'pie-inner', + outer: 'pie', + }; + const LABEL_LAYOUT_TYPE_MAP = { + inner: '', + outer: 'pie-outer', + }; + const labelType = LABEL_TYPE_MAP[labelCfg.type] || 'pie'; + const labelLayoutType = LABEL_LAYOUT_TYPE_MAP[labelCfg.type] || 'pie-outer'; + labelCfg.type = labelType; + labelCfg.layout = deepMix({}, labelCfg.layout, { type: labelLayoutType }); + geometry.label({ - fields: [angleField, colorField], + // fix: could not create scale, when field is undefined(attributes 中的 fields 定义都会被用来创建 scale) + fields: colorField ? [angleField, colorField] : [angleField], callback, cfg: labelCfg, }); @@ -176,26 +179,29 @@ function annotation(params: Params): Params { if (innerRadius && statistic) { const { title, content } = statistic; - let titleLineHeight = get(title, 'style.lineHeight'); - if (!titleLineHeight) { - titleLineHeight = get(title, 'style.fontSize', 20); - } - - let valueLineHeight = get(content, 'style.lineHeight'); - if (!valueLineHeight) { - valueLineHeight = get(content, 'style.fontSize', 20); - } - + let statisticTitle = { + type: 'text', + content: '', + }; + let statisticContent = { + type: 'text', + content: '', + }; const filterData = chart.getData(); const angleScale = chart.getScaleByField(angleField); const colorScale = chart.getScaleByField(colorField); const statisticData = getStatisticData(filterData, angleScale, colorScale); - const titleFormatter = get(title, 'formatter'); const contentFormatter = get(content, 'formatter'); - annotationOptions.push( - { + if (title !== false) { + let titleLineHeight = get(title, 'style.lineHeight'); + if (!titleLineHeight) { + titleLineHeight = get(title, 'style.fontSize', 20); + } + const titleFormatter = get(title, 'formatter'); + + statisticTitle = { type: 'text', position: ['50%', '50%'], content: titleFormatter ? titleFormatter(statisticData, filterData) : statisticData.title, @@ -204,14 +210,21 @@ function annotation(params: Params): Params { { // default config style: StatisticTitleStyle, - offsetY: -titleLineHeight, + offsetY: content === false ? 0 : -titleLineHeight, // append-info key: 'statistic', }, title ), - }, - { + }; + } + + if (content !== false) { + let valueLineHeight = get(content, 'style.lineHeight'); + if (!valueLineHeight) { + valueLineHeight = get(content, 'style.fontSize', 20); + } + statisticContent = { type: 'text', position: ['50%', '50%'], content: contentFormatter ? contentFormatter(statisticData, filterData) : statisticData.value, @@ -220,14 +233,17 @@ function annotation(params: Params): Params { { // default config style: StatisticContentStyle, - offsetY: valueLineHeight, + // 居中 + offsetY: title === false ? 0 : valueLineHeight, // append-info key: 'statistic', }, content ), - } - ); + }; + } + + annotationOptions.push(statisticTitle, statisticContent); chart.render(); } @@ -242,7 +258,7 @@ function annotation(params: Params): Params { } /** - * 折线图适配器 + * 饼图适配器 * @param chart * @param options */ diff --git a/src/plots/pie/index.ts b/src/plots/pie/index.ts index 49626e3893c..b994a6e4284 100644 --- a/src/plots/pie/index.ts +++ b/src/plots/pie/index.ts @@ -3,6 +3,7 @@ import { PieOptions } from './types'; import { adaptor } from './adaptor'; import { Adaptor } from '../../core/adaptor'; import './interaction'; +import './label'; export { PieOptions }; @@ -10,6 +11,18 @@ export class Pie extends Plot { /** 图表类型 */ public type: string = 'pie'; + /** + * 获取 饼图 默认配置项 + */ + protected getDefaultOptions(): Partial { + return { + tooltip: { + showTitle: false, + showMarkers: false, + }, + }; + } + /** * 获取 饼图 的适配器 */ diff --git a/src/plots/pie/label/index.ts b/src/plots/pie/label/index.ts new file mode 100644 index 00000000000..ba63f435a79 --- /dev/null +++ b/src/plots/pie/label/index.ts @@ -0,0 +1,4 @@ +import { registerGeometryLabel } from '@antv/g2'; +import PieInnerLabel from './inner-label'; + +registerGeometryLabel('pie-inner', PieInnerLabel); diff --git a/src/plots/pie/label/inner-label.ts b/src/plots/pie/label/inner-label.ts new file mode 100644 index 00000000000..dea8fdc99dd --- /dev/null +++ b/src/plots/pie/label/inner-label.ts @@ -0,0 +1,37 @@ +import { getGeometryLabel } from '@antv/g2'; +import { deepMix, isNil, isString } from '@antv/util'; +import { parsePercentageToNumber } from '../utils'; + +const PieLabel = getGeometryLabel('pie'); + +export default class PieInnerLabel extends PieLabel { + public defaultLayout = 'pie-inner'; + + /** + * 获取 label 的默认配置 + * - 饼图 inner-label 强制不显示 label 牵引线 + */ + protected getDefaultLabelCfg() { + const cfg = super.getDefaultLabelCfg(); + return deepMix({}, cfg, { labelLine: false }); + } + + /** + * 获取标签 offset距离(默认 -30% ) + * todo G2 offset 允许百分比设置后,移除 ts-ignore + */ + // @ts-ignore + protected getDefaultOffset(offset: number | string) { + const coordinate = this.getCoordinate(); + const radius = coordinate.getRadius(); + let innerRadius = 0; + if (coordinate.innerRadius && coordinate.radius) { + innerRadius = radius * (coordinate.innerRadius / coordinate.radius); + } + let actualOffset = offset; + if (isString(actualOffset)) { + actualOffset = (radius - innerRadius) * parsePercentageToNumber(actualOffset); + } + return isNil(actualOffset) || actualOffset > 0 ? -(radius - innerRadius) * 0.3 : actualOffset; + } +} diff --git a/src/plots/pie/types.ts b/src/plots/pie/types.ts index a7a5c309ee1..266f50d69d0 100644 --- a/src/plots/pie/types.ts +++ b/src/plots/pie/types.ts @@ -33,8 +33,11 @@ type Statistic = Readonly<{ export interface PieOptions extends Options { /** 角度映射字段 */ readonly angleField: string; + /** 颜色映射字段 */ readonly colorField?: string; + /** 饼图半径 */ readonly radius?: number; + /** 饼图内半径 */ readonly innerRadius?: number; /** 饼图图形样式 */ diff --git a/src/plots/pie/utils.ts b/src/plots/pie/utils.ts index 4987568b6d2..3e439c13bbd 100644 --- a/src/plots/pie/utils.ts +++ b/src/plots/pie/utils.ts @@ -1,6 +1,6 @@ import { Scale } from '@antv/g2/lib/dependents'; import { Data, Datum } from '@antv/g2/lib/interface'; -import { each, isArray } from '@antv/util'; +import { each, isArray, isString } from '@antv/util'; import { StatisticData } from './types'; /** @@ -42,3 +42,16 @@ export function getStatisticData(data: Data | Datum, angleScale?: Scale, colorSc value: angleScale ? angleScale.getText(data[angleField]) : data[angleField], }; } + +/** + * 将 字符串的百分比 转换为 数值百分比 + */ +export function parsePercentageToNumber(percentage: string): number { + if (!isString(percentage)) { + return percentage; + } + if (percentage.endsWith('%')) { + return Number(percentage.slice(0, -1)) * 0.01; + } + return Number(percentage); +}