diff --git a/__tests__/data/circle-packing.ts b/__tests__/data/circle-packing.ts
new file mode 100644
index 0000000000..131d728c1f
--- /dev/null
+++ b/__tests__/data/circle-packing.ts
@@ -0,0 +1,131 @@
+export const DATA = {
+ name: 'root',
+ children: [
+ {
+ name: 'Drama',
+ value: 1046790,
+ },
+ {
+ name: 'Comedy',
+ value: 1039358,
+ },
+ {
+ name: 'Documentary',
+ value: 461880,
+ },
+ {
+ name: 'News',
+ value: 308136,
+ },
+ {
+ name: 'Talk-Show',
+ value: 270578,
+ },
+ {
+ name: 'Action',
+ value: 226334,
+ },
+ {
+ name: 'Animation',
+ value: 197342,
+ },
+ {
+ name: 'Reality-TV',
+ value: 189739,
+ },
+ {
+ name: 'Crime',
+ value: 175272,
+ },
+ {
+ name: 'Family',
+ value: 150621,
+ },
+ {
+ name: 'Short',
+ value: 138255,
+ },
+ {
+ name: 'Adventure',
+ value: 121216,
+ },
+ {
+ name: 'Game-Show',
+ value: 119912,
+ },
+ {
+ name: 'Music',
+ value: 102488,
+ },
+ {
+ name: 'Adult',
+ value: 90157,
+ },
+ {
+ name: 'Biography',
+ value: 59307,
+ },
+ {
+ name: 'Sport',
+ value: 58999,
+ },
+ {
+ name: 'Romance',
+ value: 52776,
+ },
+ {
+ name: 'Horror',
+ value: 50800,
+ },
+ {
+ name: 'Fantasy',
+ value: 22614,
+ },
+ {
+ name: 'Sci-Fi',
+ value: 22026,
+ },
+ {
+ name: 'Thriller',
+ value: 19706,
+ },
+ {
+ name: 'Mystery',
+ value: 18274,
+ },
+ {
+ name: 'History',
+ value: 16108,
+ },
+ {
+ name: 'Western',
+ value: 12535,
+ },
+ {
+ name: 'Musical',
+ value: 12240,
+ },
+ {
+ name: 'War',
+ value: 1992,
+ },
+ {
+ name: 'Film-Noir',
+ value: 12240 + 1992 + 1992,
+ children: [
+ {
+ name: 'Musical',
+ value: 12240,
+ },
+ {
+ name: 'War',
+ value: 1992,
+ },
+ {
+ name: 'Film',
+ value: 1992,
+ },
+ ],
+ },
+ ],
+};
diff --git a/__tests__/unit/plots/circle-packing/animation-spec.ts b/__tests__/unit/plots/circle-packing/animation-spec.ts
new file mode 100644
index 0000000000..a168039767
--- /dev/null
+++ b/__tests__/unit/plots/circle-packing/animation-spec.ts
@@ -0,0 +1,89 @@
+import { CirclePacking } from '../../../../src';
+import { createDiv } from '../../../utils/dom';
+import { DATA } from '../../../data/circle-packing';
+
+describe('Circle-Packing', () => {
+ const div = createDiv();
+ const plot = new CirclePacking(div, {
+ autoFit: true,
+ padding: 0,
+ data: DATA,
+ animation: {
+ appear: {
+ animation: 'zoom-in',
+ duration: 500,
+ },
+ leave: {
+ animation: 'zoom-out',
+ duration: 500,
+ },
+ },
+ });
+ plot.render();
+
+ it('default', () => {
+ //
+ expect(plot.chart.geometries[0].animateOption).toEqual({
+ appear: {
+ animation: 'zoom-in',
+ duration: 500,
+ easing: 'easeQuadOut',
+ },
+ update: {
+ duration: 400,
+ easing: 'easeQuadInOut',
+ },
+ enter: {
+ duration: 400,
+ easing: 'easeQuadInOut',
+ animation: 'zoom-in',
+ },
+ leave: {
+ duration: 500,
+ easing: 'easeQuadIn',
+ animation: 'zoom-out',
+ },
+ });
+ });
+
+ it('update', () => {
+ plot.update({
+ animation: {
+ appear: {
+ animation: 'fade-in',
+ },
+ enter: {
+ animation: 'fade-in',
+ },
+ leave: {
+ animation: 'wave-out',
+ },
+ },
+ });
+ expect(plot.chart.geometries[0].animateOption).toEqual({
+ appear: {
+ animation: 'fade-in',
+ duration: 500,
+ easing: 'easeQuadOut',
+ },
+ update: {
+ duration: 400,
+ easing: 'easeQuadInOut',
+ },
+ enter: {
+ duration: 400,
+ easing: 'easeQuadInOut',
+ animation: 'fade-in',
+ },
+ leave: {
+ duration: 500,
+ easing: 'easeQuadIn',
+ animation: 'wave-out',
+ },
+ });
+ });
+
+ afterAll(() => {
+ plot.destroy();
+ });
+});
diff --git a/__tests__/unit/plots/circle-packing/index-spec.ts b/__tests__/unit/plots/circle-packing/index-spec.ts
new file mode 100644
index 0000000000..6024b0211e
--- /dev/null
+++ b/__tests__/unit/plots/circle-packing/index-spec.ts
@@ -0,0 +1,134 @@
+import { CirclePacking } from '../../../../src';
+import { createDiv } from '../../../utils/dom';
+import { DATA } from '../../../data/circle-packing';
+import { DEFAULT_OPTIONS } from '../../../../src/plots/circle-packing/constant';
+import { getContainerSize } from '../../../../src/utils';
+
+describe('Circle-Packing', () => {
+ const div = createDiv();
+ const plot = new CirclePacking(div, {
+ padding: 0,
+ data: DATA,
+ legend: false,
+ hierarchyConfig: {
+ sort: (a, b) => b.depth - a.depth,
+ },
+ });
+ plot.render();
+
+ it('default', () => {
+ expect(plot.type).toBe('circle-packing');
+ // @ts-ignore
+ expect(plot.getDefaultOptions()).toBe(CirclePacking.getDefaultOptions());
+
+ const geometry = plot.chart.geometries[0];
+ expect(geometry.type).toBe('point');
+
+ const positionFields = geometry.getAttribute('position').getFields();
+ expect(geometry.elements.length).toBe(geometry.data.length);
+ expect(positionFields).toHaveLength(2);
+ expect(positionFields).toEqual(['x', 'y']);
+
+ // 圆形布局 宽高一致,即正常
+ const coordinateBox = plot.chart.coordinateBBox;
+ const { width, height } = plot.chart.viewBBox;
+ const minSize = Math.min(width, height);
+ expect(coordinateBox.width).toBe(coordinateBox.height);
+ expect(minSize).toBe(coordinateBox.width);
+ expect(minSize).toBe(coordinateBox.height);
+ });
+
+ it('color', () => {
+ plot.update({ color: ['red', 'green', 'blue'] });
+
+ const geometry = plot.chart.geometries[0];
+ const elements = geometry.elements;
+ expect(elements.length).toBe(plot.chart.getData().length);
+
+ // 绘图数据
+ expect(elements[0].getModel().color).toBe('red');
+ expect(elements[1].getModel().color).toBe('green');
+ expect(elements[7].getModel().color).toBe('green');
+ expect(elements[14].getModel().color).toBe('blue' /** 15 % 3 === 0 */);
+ });
+
+ it('style', () => {
+ plot.update({ pointStyle: { fill: 'red', fillOpacity: 1 } });
+
+ const geometry = plot.chart.geometries[0];
+ let elements = geometry.elements;
+ expect(elements.length).toBe(plot.chart.getData().length);
+
+ // 绘图数据
+ expect(elements[0].shape.attr('fillOpacity')).toBe(1);
+ expect(elements[1].shape.attr('fillOpacity')).toBe(1);
+ expect(elements[1].shape.attr('fill')).toBe('red');
+ expect(elements[7].shape.attr('fillOpacity')).toBe(1);
+ expect(elements[13].shape.attr('fillOpacity')).toBe(1);
+ expect(elements[14].shape.attr('fillOpacity')).toBe(1);
+
+ // callback
+ plot.update({
+ rawFields: ['depth'],
+ pointStyle: ({ depth }) => ({
+ fill: 'red',
+ fillOpacity: depth > 1 ? 1 : 0.5,
+ stroke: 'green',
+ lineWidth: depth,
+ }),
+ });
+ elements = plot.chart.geometries[0].elements;
+ // 绘图数据
+ expect(elements[0].shape.attr('fillOpacity')).toBe(0.5);
+ expect(elements[0].shape.attr('stroke')).toBe('green');
+
+ expect(elements[0].shape.attr('fillOpacity')).toBe(0.5);
+ expect(elements[elements.length - 1].shape.attr('fillOpacity')).toBe(1);
+
+ expect(elements[0].shape.attr('lineWidth')).toBe(0);
+ expect(elements[1].shape.attr('lineWidth')).toBe(1);
+ expect(elements[elements.length - 1].shape.attr('lineWidth')).toBe(2);
+ });
+
+ it('label', () => {
+ let geometry = plot.chart.geometries[0];
+ let labelGroup = plot.chart.geometries[0].labelsContainer.getChildren()[0];
+ expect(typeof plot.chart.geometries[0].labelOption).toBe('object');
+ // @ts-ignore
+ expect(labelGroup.getChildByIndex(0).attr('text')).toBe(DATA.name);
+ // @ts-ignore
+ expect(plot.chart.geometries[0].labelsContainer.getChildren()[1].getChildByIndex(0).attr('text')).toBe(
+ DATA.children[0].name
+ );
+
+ plot.update({ label: { fields: ['value'] } });
+ labelGroup = plot.chart.geometries[0].labelsContainer.getChildren()[0];
+ const filterData = plot.chart.getData();
+ // @ts-ignore
+ expect(labelGroup.getChildByIndex(0).attr('text')).toBe(`${filterData[0].value}`);
+
+ // meta
+ plot.update({ meta: { value: { formatter: (v) => v + '%' } } });
+ labelGroup = plot.chart.geometries[0].labelsContainer.getChildren()[0];
+ // @ts-ignore
+ expect(labelGroup.getChildByIndex(0).attr('text')).toBe(`${filterData[0].value}%`);
+
+ // formatter
+ plot.update({ label: { formatter: () => 'xxx' } });
+ geometry = plot.chart.geometries[0];
+ // @ts-ignore
+ expect(geometry.labelsContainer.getChildren()[0].getChildByIndex(0).attr('text')).toBe('xxx');
+
+ // 关闭
+ plot.update({ label: false });
+ expect(plot.chart.geometries[0].labelOption).toBe(false);
+ });
+
+ it('defaultOptions 保持从 constants 中获取', () => {
+ expect(CirclePacking.getDefaultOptions()).toEqual(DEFAULT_OPTIONS);
+ });
+
+ afterAll(() => {
+ plot.destroy();
+ });
+});
diff --git a/__tests__/unit/plots/circle-packing/tooltip-spec.ts b/__tests__/unit/plots/circle-packing/tooltip-spec.ts
new file mode 100644
index 0000000000..3fe3910563
--- /dev/null
+++ b/__tests__/unit/plots/circle-packing/tooltip-spec.ts
@@ -0,0 +1,149 @@
+import { TooltipCfg } from '@antv/g2/lib/interface';
+import { CirclePacking } from '../../../../src';
+import { createDiv, removeDom } from '../../../utils/dom';
+import { DATA } from '../../../data/circle-packing';
+import { DEFAULT_OPTIONS } from '../../../../src/plots/circle-packing/constant';
+
+describe('Circle-Packing', () => {
+ const div = createDiv();
+ const plot = new CirclePacking(div, {
+ autoFit: true,
+ padding: 0,
+ data: DATA,
+ label: false,
+ });
+ plot.render();
+
+ it('default', () => {
+ // 默认有tooltip
+ const tooltipOptions = plot.chart.getOptions().tooltip as TooltipCfg;
+ expect(tooltipOptions).not.toBe(false);
+ expect(tooltipOptions).toMatchObject(DEFAULT_OPTIONS.tooltip);
+ // @ts-ignore 默认不展示 markers 和 title
+ expect(tooltipOptions.showMarkers).toBe(false);
+ expect(tooltipOptions.showTitle).toBe(false);
+ // @ts-ignore isVisible
+ expect(plot.chart.getController('tooltip').isVisible()).toBe(true);
+ });
+
+ it('meta', () => {
+ plot.update({
+ meta: {
+ name: {
+ formatter: (v) => `名称:${v}`,
+ },
+ value: {
+ formatter: (v) => `值:${v} `,
+ },
+ },
+ });
+ const tooltipController = plot.chart.getController('tooltip');
+ const box = plot.chart.geometries[0].elements[2].shape.getBBox();
+ const point = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
+
+ const items = tooltipController.getTooltipItems(point);
+ expect(items.length).toBe(1);
+ expect(items[0].name).toBe('名称:Comedy');
+
+ plot.chart.showTooltip(point);
+ expect(div.querySelectorAll('.g2-tooltip-list-item').length).toBe(1);
+ expect((div.querySelector('.g2-tooltip-name') as HTMLElement).innerText).toBe(`名称:Comedy`);
+ expect((div.querySelector('.g2-tooltip-value') as HTMLElement).innerText).toBe(`值:1039358`);
+ plot.chart.hideTooltip();
+ });
+
+ it('tooltip: fields', () => {
+ plot.update({ meta: undefined });
+ const tooltipController = plot.chart.getController('tooltip');
+ const box = plot.chart.geometries[0].elements[2].shape.getBBox();
+ const point = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
+
+ plot.chart.showTooltip(point);
+ expect(div.querySelectorAll('.g2-tooltip-list-item').length).toBe(1);
+ plot.chart.hideTooltip();
+
+ plot.update({
+ meta: {
+ name: { alias: '名称', formatter: (v) => `🌞 ${v}` },
+ value: { alias: '数值' },
+ depth: { alias: '深度' },
+ },
+ tooltip: {
+ fields: ['name', 'value', 'depth'],
+ },
+ });
+ const items = tooltipController.getTooltipItems(point);
+ expect(items.length).toBe(3);
+ expect(items[0].name).toBe('名称');
+ expect(items[1].name).toBe('数值');
+ expect(items[2].name).toBe('深度');
+
+ plot.chart.showTooltip(point);
+ expect(div.querySelectorAll('.g2-tooltip-list-item').length).toBe(3);
+ expect((div.querySelector('.g2-tooltip-name') as HTMLElement).innerText).toBe('名称');
+ expect((div.querySelectorAll('.g2-tooltip-value')[1] as HTMLElement).innerText).toBe(
+ `${plot.chart.getData()[2].value}`
+ );
+ expect((div.querySelectorAll('.g2-tooltip-value')[2] as HTMLElement).innerText).toBe(
+ `${plot.chart.getData()[2].depth}`
+ );
+ plot.chart.hideTooltip();
+ });
+
+ it('tooltip: formatter', () => {
+ plot.update({
+ tooltip: {
+ fields: ['name', 'value', 'path'],
+ formatter: () => ({ name: 'name', value: 'value' }),
+ },
+ });
+ const tooltipController = plot.chart.getController('tooltip');
+ const box = plot.chart.geometries[0].elements[0].shape.getBBox();
+ const point = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
+
+ const items = tooltipController.getTooltipItems(point);
+ // fixme G2 现在的 bug,只能展示一条
+ expect(items.length).toBe(1);
+ expect(items[0].name).toBe('name');
+ expect(items[0].value).toBe('value');
+
+ plot.chart.showTooltip(point);
+ expect(div.querySelectorAll('.g2-tooltip-list-item').length).toBe(1);
+ expect((div.querySelector('.g2-tooltip-name') as HTMLElement).innerText).toBe('name');
+ expect((div.querySelector('.g2-tooltip-value') as HTMLElement).innerText).toBe('value');
+ plot.chart.hideTooltip();
+ });
+
+ it('tooltip: customContent', () => {
+ plot.update({
+ meta: undefined,
+ tooltip: {
+ fields: ['name', 'value'],
+ formatter: undefined,
+ customContent: (title, items) =>
+ `
${items.map((item) => `${item.value}`)}
`,
+ },
+ });
+ const box = plot.chart.geometries[0].elements[1].shape.getBBox();
+ const point = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
+
+ plot.chart.showTooltip(point);
+ const chartData = plot.chart.getData();
+ expect((div.querySelectorAll('.custom-tooltip-value')[0] as HTMLElement).innerText).toBe(`${chartData[1].name}`);
+ expect((div.querySelectorAll('.custom-tooltip-value')[1] as HTMLElement).innerText).toBe(`${chartData[1].value}`);
+ plot.chart.hideTooltip();
+ });
+
+ it('tooltip: hide', () => {
+ plot.update({ tooltip: false });
+ // @ts-ignore
+ expect(plot.chart.options.tooltip).toBe(false);
+ // @ts-ignore
+ expect(plot.chart.getController('tooltip').isVisible()).toBe(false);
+ });
+
+ afterAll(() => {
+ plot.destroy();
+ removeDom(div);
+ });
+});
diff --git a/docs/api/plots/circle-packing.en.md b/docs/api/plots/circle-packing.en.md
new file mode 100644
index 0000000000..69e0045ba6
--- /dev/null
+++ b/docs/api/plots/circle-packing.en.md
@@ -0,0 +1,187 @@
+---
+title: Circle packing
+order: 40
+---
+
+### Plot Container
+
+`markdown:docs/common/chart-options.en.md`
+
+### Data Mapping
+
+#### data
+
+**required** _object_
+
+Configure the chart data source. For Circle packing:
+
+```sign
+type Node = { name: string; value?: number; children: Node[]; }
+```
+
+示例:
+
+```ts
+{
+ name: 'root',
+ children: [
+ { name: 'type1', value: 1 },
+ { name: 'type2', value: 3, children: [{ name: 'type2-1', value: 2 }] }
+ ]
+}
+```
+
+#### meta
+
+`markdown:docs/common/meta.en.md`
+
+
+Circle packing contains data fields such as 'x', 'y', 'r', 'name', 'value', 'path', and 'depth', which can be retrieved from the metadata (used in tooltip and style callbacks).
+
+You can set the meta information of a field as follows:
+
+```ts
+meta: {
+ name: {
+ formatter: (v) => `名称:${v}`,
+ },
+ value: {
+ alias: '值',
+ },
+ depth: {
+ alias: '深度',
+ }
+},
+```
+
+#### colorField
+
+**optional** _string_
+
+Color mapping field. The default is: 'name', and the colors are sorted by name field.
+
+#### sizeField
+
+**optional** _string_
+
+The name of the data field corresponding to the point size map.
+
+#### rawFields
+
+**optional** _string[]_
+
+Extra original fields. Once configured, you can retrieve additional raw data in the datum parameter of callback functions such as Tooltip.
+
+### Geometry Style
+
+#### hierarchyConfig ✨
+
+**optional** _object_
+
+Hierarchy configuration, such as' size ', 'padding', etc., refer to [D3-Hierarchy](https://github.com/d3/d3-hierarchy#pack) for detailed configuration.
+
+Supports configuration properties:
+
+| Properties | Type | Description | |
+| ------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
+| field | _string_ | The data node weight mapping field, default is: 'value'. When your node data format is not: '{name: 'xx', value: 'xx'} ', you can use this field to specify. See the example chart for details |
+| padding | _number\|number[]_ | default: `0`。reference:[d3-hierarchy#pack_padding](https://github.com/d3/d3-hierarchy#pack_padding) |
+| size | _number[]_ | default: `[1, 1]`。reference:[d3-hierarchy#pack_size](https://github.com/d3/d3-hierarchy#pack_size) |
+| sort | _Function_ | Data node sorting method, default: descending order.reference: [d3-hierarchy#node_sort](https://github.com/d3/d3-hierarchy#node_sort) |
+
+
+
+
+`markdown:docs/common/color.en.md`
+
+#### pointStyle
+
+**optional** _object_
+
+Set the point style. The `fill` in pointStyle overrides the configuration of `color`. PointStyle can be specified either directly or via a callback to specify individual styles based on the data.
+
+Default configuration:
+
+| Properties | Type | Description |
+| ------------- | ------ | --------------------- |
+| fill | string | Fill color |
+| stroke | string | Stroke color |
+| lineWidth | number | Line width |
+| lineDash | number | The dotted lines show |
+| opacity | number | Transparency |
+| fillOpacity | number | Fill transparency |
+| strokeOpacity | number | Stroke transparency |
+
+```ts
+// Specified directly
+{
+ pointStyle: {
+ fill: 'red',
+ stroke: 'yellow',
+ opacity: 0.8
+ },
+}
+// Function
+{
+ pointStyle: ({ value }) => {
+ if (value > 50000) {
+ return {
+ fill: 'green',
+ stroke: 'yellow',
+ opacity: 0.8,
+ }
+ }
+ // TODO
+ return {
+ fill: 'red',
+ stroke: 'yellow',
+ opacity: 0.8,
+ }
+ }
+}
+```
+
+#### reflect
+
+**optional** _x | y_
+
+You can use `reflect: 'x'` to carry out an X-axis reverse and `reflect: 'y'` to carry out a Y-axis reverse.
+
+### Plot Components
+
+`markdown:docs/common/component-polygon.en.md`
+
+### Plot Interactions
+
+
+
+
+`markdown:docs/common/interactions.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/circle-packing.zh.md b/docs/api/plots/circle-packing.zh.md
new file mode 100644
index 0000000000..188b6cd95a
--- /dev/null
+++ b/docs/api/plots/circle-packing.zh.md
@@ -0,0 +1,188 @@
+---
+title: Circle packing
+order: 40
+---
+
+### 图表容器
+
+`markdown:docs/common/chart-options.zh.md`
+
+### 数据映射
+
+#### data
+
+**required** _object_
+
+设置图表数据源。 Circle Packing 的数据格式要求为:
+
+```sign
+type Node = { name: string; value?: number; children: Node[]; }
+```
+
+示例:
+
+```ts
+{
+ name: 'root',
+ children: [
+ { name: 'type1', value: 1 },
+ { name: 'type2', value: 3, children: [{ name: 'type2-1', value: 2 }] }
+ ]
+}
+```
+
+#### meta
+
+`markdown:docs/common/meta.zh.md`
+
+Circle packing 内含的数据字段有:'x', 'y', 'r', 'name', 'value', 'path', 'depth', 这些字段可以在元数据中获取(tooltip、style 回调中使用).
+
+可以通过下面的方式来设置字段的元信息:
+
+```ts
+meta: {
+ name: {
+ formatter: (v) => `名称:${v}`,
+ },
+ value: {
+ alias: '值',
+ },
+ depth: {
+ alias: '深度',
+ }
+},
+```
+
+#### colorField
+
+**optional** _string_
+
+颜色映射字段。默认为:`name`,按照 name 字段对颜色进行分类。
+
+#### sizeField
+
+**optional** _string_
+
+点大小映射对应的数据字段名。
+
+#### rawFields
+
+**optional** _string[]_
+
+额外的原始字段。配置之后,可以在 tooltip 等回调函数的 datum 参数中,获取到更多额外的原始数据。
+
+
+
+
+### 图形样式
+
+#### hierarchyConfig ✨
+
+**optional** _object_
+
+层级布局配置,参考[d3-hierarchy](https://github.com/d3/d3-hierarchy#pack)。
+
+支持配置属性:
+
+| 属性 | 类型 | 描述 | |
+| ------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
+| field | _string_ | 数据节点权重映射字段,默认为:`value`. 当你的节点数据格式不是:`{ name: 'xx', value: 'xx' }`, 可以通过该字段来指定,详细见:图表示例 |
+| padding | _number\|number[]_ | 默认:`0`。参考:[d3-hierarchy#pack_padding](https://github.com/d3/d3-hierarchy#pack_padding) |
+| size | _number[]_ | 默认:`[1, 1]`。参考:[d3-hierarchy#pack_size](https://github.com/d3/d3-hierarchy#pack_size) |
+| sort | _Function_ | 数据节点排序方式,默认:降序。参考: [d3-hierarchy#node_sort](https://github.com/d3/d3-hierarchy#node_sort) |
+
+
+
+
+
+
+
+
+`markdown:docs/common/color.zh.md`
+
+#### pointStyle
+
+**optional** _object_
+
+设置点样式。pointStyle 中的`fill`会覆盖 `color` 的配置。pointStyle 可以直接指定,也可以通过 callback 的方式,根据数据指定单独的样式。
+
+默认配置:
+
+| 细分配置 | 类型 | 功能描述 |
+| ------------- | ------ | ---------- |
+| fill | string | 填充颜色 |
+| stroke | string | 描边颜色 |
+| lineWidth | number | 线宽 |
+| lineDash | number | 虚线显示 |
+| opacity | number | 透明度 |
+| fillOpacity | number | 填充透明度 |
+| strokeOpacity | number | 描边透明度 |
+
+```ts
+// 直接指定
+{
+ pointStyle: {
+ fill: 'red',
+ stroke: 'yellow',
+ opacity: 0.8
+ },
+}
+// Function
+{
+ pointStyle: ({ value }) => {
+ if (value > 50000) {
+ return {
+ fill: 'green',
+ stroke: 'yellow',
+ opacity: 0.8,
+ }
+ }
+ // TODO
+ return {
+ fill: 'red',
+ stroke: 'yellow',
+ opacity: 0.8,
+ }
+ }
+}
+```
+
+
+#### reflect
+
+**optional** _x | y_
+
+可使用 `reflect: 'x'` 进行 x 轴反转,使用 `reflect: 'y'` 进行 y 轴反转。
+
+### 图表组件
+
+`markdown:docs/common/component-polygon.zh.md`
+
+### 图表交互
+
+
+
+
+
+`markdown:docs/common/interactions.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/circle-packing/API.en.md b/examples/more-plots/circle-packing/API.en.md
new file mode 100644
index 0000000000..460510625f
--- /dev/null
+++ b/examples/more-plots/circle-packing/API.en.md
@@ -0,0 +1 @@
+`markdown:docs/api/plots/circle-packing.en.md`
\ No newline at end of file
diff --git a/examples/more-plots/circle-packing/API.zh.md b/examples/more-plots/circle-packing/API.zh.md
new file mode 100644
index 0000000000..92149474ce
--- /dev/null
+++ b/examples/more-plots/circle-packing/API.zh.md
@@ -0,0 +1 @@
+`markdown:docs/api/plots/circle-packing.zh.md`
\ No newline at end of file
diff --git a/examples/more-plots/circle-packing/demo/basic.ts b/examples/more-plots/circle-packing/demo/basic.ts
new file mode 100644
index 0000000000..4185118390
--- /dev/null
+++ b/examples/more-plots/circle-packing/demo/basic.ts
@@ -0,0 +1,17 @@
+import { CirclePacking } from '@antv/g2plot';
+
+fetch('https://gw.alipayobjects.com/os/antfincdn/%24m0nDoQYqH/basic-packing.json')
+ .then((data) => data.json())
+ .then((data) => {
+ const plot = new CirclePacking('container', {
+ autoFit: true,
+ data,
+ label: false,
+ legend: false,
+ hierarchyConfig: {
+ sort: (a, b) => b.depth - a.depth,
+ },
+ });
+
+ plot.render();
+ });
diff --git a/examples/more-plots/circle-packing/demo/custom-padding.ts b/examples/more-plots/circle-packing/demo/custom-padding.ts
new file mode 100644
index 0000000000..2f44038a77
--- /dev/null
+++ b/examples/more-plots/circle-packing/demo/custom-padding.ts
@@ -0,0 +1,19 @@
+import { CirclePacking } from '@antv/g2plot';
+fetch('https://gw.alipayobjects.com/os/antfincdn/%24m0nDoQYqH/basic-packing.json')
+ .then((data) => data.json())
+ .then((data) => {
+ const plot = new CirclePacking('container', {
+ autoFit: true,
+ padding: 0,
+ data,
+ hierarchyConfig: {
+ padding: 0.01,
+ },
+ color: 'rgb(252, 253, 191)-rgb(231, 82, 99)-rgb(183, 55, 121)',
+ // 自定义 label 样式
+ label: false,
+ legend: false,
+ });
+
+ plot.render();
+ });
diff --git a/examples/more-plots/circle-packing/demo/label.ts b/examples/more-plots/circle-packing/demo/label.ts
new file mode 100644
index 0000000000..edddf843a3
--- /dev/null
+++ b/examples/more-plots/circle-packing/demo/label.ts
@@ -0,0 +1,28 @@
+import { CirclePacking } from '@antv/g2plot';
+fetch('https://gw.alipayobjects.com/os/antfincdn/%24m0nDoQYqH/basic-packing.json')
+ .then((data) => data.json())
+ .then((data) => {
+ const plot = new CirclePacking('container', {
+ autoFit: true,
+ padding: 0,
+ data,
+ sizeField: 'r',
+ color: 'rgb(252, 253, 191)-rgb(231, 82, 99)-rgb(183, 55, 121)',
+ // 自定义 label 样式
+ label: {
+ formatter: ({ name }) => {
+ return name !== 'root' ? name : '';
+ },
+ // 偏移
+ offsetY: 8,
+ style: {
+ fontSize: 12,
+ textAlign: 'center',
+ fill: 'rgba(0,0,0,0.65)',
+ },
+ },
+ legend: false,
+ });
+
+ plot.render();
+ });
diff --git a/examples/more-plots/circle-packing/demo/meta.json b/examples/more-plots/circle-packing/demo/meta.json
new file mode 100644
index 0000000000..4436fe01c2
--- /dev/null
+++ b/examples/more-plots/circle-packing/demo/meta.json
@@ -0,0 +1,40 @@
+{
+ "title": {
+ "zh": "中文分类",
+ "en": "Category"
+ },
+ "demos": [
+ {
+ "filename": "basic.ts",
+ "title": {
+ "zh": "Circle packing",
+ "en": "Circle packing"
+ },
+ "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/AF1%24co0stf/7db34ba1-5adc-4fca-ab40-fdeb45195ffb.png"
+ },
+ {
+ "filename": "nest.ts",
+ "title": {
+ "zh": "多层 circle packing",
+ "en": "Nest circle packing"
+ },
+ "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/0OctH62S1l/fb31a01b-f609-4239-a5c3-358da994432e.png"
+ },
+ {
+ "filename": "label.ts",
+ "title": {
+ "zh": "展示数据标签",
+ "en": "Display label"
+ },
+ "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/0OctH62S1l/fb31a01b-f609-4239-a5c3-358da994432e.png"
+ },
+ {
+ "filename": "custom-padding.ts",
+ "title": {
+ "zh": "自定义圆圈之间的 padding 距离",
+ "en": "Custom padding between circles"
+ },
+ "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/dCP6LawPW0/7ffa2fef-af09-4df4-8db3-b8f1ddbc63a5.png"
+ }
+ ]
+}
diff --git a/examples/more-plots/circle-packing/demo/nest.ts b/examples/more-plots/circle-packing/demo/nest.ts
new file mode 100644
index 0000000000..bb26b9bf5f
--- /dev/null
+++ b/examples/more-plots/circle-packing/demo/nest.ts
@@ -0,0 +1,30 @@
+import { CirclePacking } from '@antv/g2plot';
+
+fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/flare.json')
+ .then((data) => data.json())
+ .then((data) => {
+ const plot = new CirclePacking('container', {
+ autoFit: true,
+ padding: 0,
+ data,
+ sizeField: 'r',
+ // 自定义颜色
+ colorField: 'r',
+ color: 'rgb(252, 253, 191)-rgb(231, 82, 99)-rgb(183, 55, 121)',
+ // 自定义样式
+ pointStyle: {
+ stroke: 'rgb(183, 55, 121)',
+ lineWidth: 0.5,
+ },
+ label: false,
+ legend: false,
+ drilldown: {
+ enabled: true,
+ breadCrumb: {
+ position: 'top-left',
+ },
+ },
+ });
+
+ plot.render();
+ });
diff --git a/examples/more-plots/circle-packing/index.en.md b/examples/more-plots/circle-packing/index.en.md
new file mode 100644
index 0000000000..16e4d81039
--- /dev/null
+++ b/examples/more-plots/circle-packing/index.en.md
@@ -0,0 +1,4 @@
+---
+title: Circle Packing
+order: 40
+---
diff --git a/examples/more-plots/circle-packing/index.zh.md b/examples/more-plots/circle-packing/index.zh.md
new file mode 100644
index 0000000000..16e4d81039
--- /dev/null
+++ b/examples/more-plots/circle-packing/index.zh.md
@@ -0,0 +1,4 @@
+---
+title: Circle Packing
+order: 40
+---
diff --git a/package.json b/package.json
index da0f14fd0c..d0012570bc 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,7 @@
},
"dependencies": {
"@antv/event-emitter": "^0.1.2",
- "@antv/g2": "^4.1.0",
+ "@antv/g2": "^4.1.19",
"d3-hierarchy": "^2.0.0",
"d3-regression": "^1.3.5",
"pdfast": "^0.2.0",
diff --git a/src/index.ts b/src/index.ts
index e3c6d6284d..d7ab9baeea 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -165,6 +165,10 @@ export type { MixOptions } from './plots/mix';
export { Facet } from './plots/facet';
export type { FacetOptions } from './plots/facet';
+// 分面图及类型定义 | author by [visiky](https://github.com/visiky), [Angeli](https://github.com/Angelii)
+export { CirclePacking } from './plots/circle-packing';
+export type { CirclePackingOptions } from './plots/circle-packing';
+
/** 开发 adaptor 可能会用到的方法或一些工具方法,不强制使用 */
export { flow, measureTextWidth } from './utils';
diff --git a/src/interactions/actions/drill-down.ts b/src/interactions/actions/drill-down.ts
index cca6057eae..d86d0339ab 100644
--- a/src/interactions/actions/drill-down.ts
+++ b/src/interactions/actions/drill-down.ts
@@ -95,9 +95,10 @@ export class DrillDownAction extends Action {
const { position } = this.getButtonCfg();
// 默认,左上角直接出发
- let point = { x: 0, y: 0 };
+ let point = coordinate.convert({ x: 0, y: 1 });
if (position === 'bottom-left') {
- point = coordinate.isPolar ? { x: 0, y: coordinate.getHeight() } : coordinate.convert({ x: 0, y: 1 });
+ // 涉及到坐标反转的问题
+ point = coordinate.convert({ x: 0, y: 0 });
}
/** PADDING_LEFT, PADDING_TOP 与画布边缘的距离 */
const matrix = Util.transform(null, [['t', point.x + PADDING_LEFT, point.y + bbox.height + PADDING_TOP]]);
@@ -156,7 +157,7 @@ export class DrillDownAction extends Action {
}
const { view } = this.context;
- const data = last(historyCache).children;
+ const data = last(historyCache).children; // 处理后的数组
view.changeData(data);
if (historyCache.length > 1) {
diff --git a/src/plots/_template/adaptor.ts b/src/plots/_template/adaptor.ts
index 2acb6a9344..80bf6e5f6c 100644
--- a/src/plots/_template/adaptor.ts
+++ b/src/plots/_template/adaptor.ts
@@ -41,11 +41,11 @@ export function meta(params: Params): Params {
export function adaptor(params: Params) {
// flow 的方式处理所有的配置到 G2 API
return flow(
+ theme,
geometry,
meta,
interaction,
- animation,
- theme
+ animation
// ... 其他的 adaptor flow
)(params);
}
diff --git a/src/plots/circle-packing/adaptor.ts b/src/plots/circle-packing/adaptor.ts
new file mode 100644
index 0000000000..baeeac7b7e
--- /dev/null
+++ b/src/plots/circle-packing/adaptor.ts
@@ -0,0 +1,221 @@
+import { get } from '@antv/util';
+import { Types } from '@antv/g2';
+import { point } from '../../adaptor/geometries/point';
+import { Params } from '../../core/adaptor';
+import { interaction as baseInteraction, animation, theme, legend, annotation, scale } from '../../adaptor/common';
+import { flow, deepAssign } from '../../utils';
+import { getAdjustAppendPadding } from '../../utils/padding';
+import { transformData, resolvePaddingForCircle, resolveAllPadding } from './utils';
+import { CirclePackingOptions } from './types';
+import { RAW_FIELDS } from './constant';
+
+/**
+ * 获取默认 option
+ * @param params
+ */
+function defaultOptions(params: Params): Params {
+ const { chart } = params;
+ const diameter = Math.min(chart.viewBBox.width, chart.viewBBox.height);
+
+ return deepAssign(
+ {
+ options: {
+ size: ({ r }) => r * diameter, // 当autofit:false时,默认给固定半径
+ },
+ },
+ params
+ );
+}
+
+/**
+ * padding 配置
+ * @param params
+ */
+function padding(params: Params): Params {
+ const { options, chart } = params;
+ // 通过改变 padding,修改 coordinate 的绘制区域
+ const containerSize = chart.viewBBox;
+ const { padding, appendPadding, drilldown } = options;
+
+ let tempAppendPadding = appendPadding;
+ if (drilldown?.enabled) {
+ const appendPaddingByDrilldown = getAdjustAppendPadding(
+ chart.appendPadding,
+ get(drilldown, ['breadCrumb', 'position'])
+ );
+ tempAppendPadding = resolveAllPadding([appendPaddingByDrilldown, appendPadding]);
+ }
+
+ const { finalPadding } = resolvePaddingForCircle(padding, tempAppendPadding, containerSize);
+ chart.padding = finalPadding;
+ chart.appendPadding = 0;
+
+ return params;
+}
+
+/**
+ * 字段
+ * @param params
+ */
+function geometry(params: Params): Params {
+ const { chart, options } = params;
+ const { padding, appendPadding } = chart;
+ const { color, colorField, pointStyle, hierarchyConfig, sizeField, rawFields = [], drilldown } = options;
+
+ const data = transformData({
+ data: options.data,
+ hierarchyConfig,
+ enableDrillDown: drilldown?.enabled,
+ rawFields,
+ });
+ chart.data(data);
+
+ const containerSize = chart.viewBBox;
+ const { finalSize } = resolvePaddingForCircle(padding, appendPadding, containerSize);
+ // 有sizeField的时候,例如 value ,可以选择映射 size 函数,自己计算出映射的半径
+ let circleSize = ({ r }) => r * finalSize; // 默认配置
+
+ if (sizeField) {
+ circleSize = (d) => d[sizeField] * finalSize; // 目前只有 r 通道映射效果会正常
+ }
+
+ // geometry
+ point(
+ deepAssign({}, params, {
+ options: {
+ xField: 'x',
+ yField: 'y',
+ seriesField: colorField,
+ sizeField,
+ rawFields: [...RAW_FIELDS, ...rawFields],
+ point: {
+ color,
+ style: pointStyle,
+ shape: 'circle',
+ size: circleSize,
+ },
+ },
+ })
+ );
+
+ return params;
+}
+
+/**
+ * meta 配置
+ * @param params
+ */
+export function meta(params: Params): Params {
+ return flow(
+ scale(
+ {},
+ {
+ // 必须强制为 nice
+ x: { min: 0, max: 1, minLimit: 0, maxLimit: 1, nice: true },
+ y: { min: 0, max: 1, minLimit: 0, maxLimit: 1, nice: true },
+ }
+ )
+ )(params);
+}
+
+/**
+ * tooltip 配置
+ * @param params
+ */
+function tooltip(params: Params): Params {
+ const { chart, options } = params;
+ const { tooltip } = options;
+
+ if (tooltip === false) {
+ chart.tooltip(false);
+ } else {
+ let tooltipOptions = tooltip;
+ // 设置了 fields,就不进行 customItems 了; 设置 formatter 时,需要搭配 fields
+ if (!get(tooltip, 'fields')) {
+ tooltipOptions = deepAssign(
+ {},
+ {
+ customItems: (items: Types.TooltipItem[]) =>
+ items.map((item) => {
+ const scales = get(chart.getOptions(), 'scales');
+ const nameFormatter = get(scales, ['name', 'formatter'], (v) => v);
+ const valueFormatter = get(scales, ['value', 'formatter'], (v) => v);
+ return {
+ ...item,
+ name: nameFormatter(item.data.name),
+ value: valueFormatter(item.data.value),
+ };
+ }),
+ },
+ tooltipOptions
+ );
+ }
+ chart.tooltip(tooltipOptions);
+ }
+
+ return params;
+}
+
+/**
+ * 坐标轴, 默认关闭
+ * @param params
+ */
+function axis(params: Params): Params {
+ const { chart } = params;
+ chart.axis(false);
+ return params;
+}
+
+function adaptorInteraction(options: CirclePackingOptions): CirclePackingOptions {
+ const { drilldown, interactions = [] } = options;
+
+ if (drilldown?.enabled) {
+ return deepAssign({}, options, {
+ interactions: [
+ ...interactions,
+ {
+ type: 'drill-down',
+ cfg: { drillDownConfig: drilldown, transformData, enableDrillDown: true },
+ },
+ ],
+ });
+ }
+ return options;
+}
+
+/**
+ * 交互配置
+ * @param params
+ * @returns
+ */
+function interaction(params: Params): Params {
+ const { chart, options } = params;
+
+ baseInteraction({
+ chart,
+ options: adaptorInteraction(options),
+ });
+
+ return params;
+}
+
+/**
+ * 矩形树图
+ * @param chart
+ * @param options
+ */
+export function adaptor(params: Params) {
+ return flow(
+ defaultOptions,
+ padding,
+ theme,
+ meta,
+ geometry,
+ axis,
+ legend,
+ tooltip,
+ interaction,
+ animation,
+ annotation()
+ )(params);
+}
diff --git a/src/plots/circle-packing/constant.ts b/src/plots/circle-packing/constant.ts
new file mode 100644
index 0000000000..3f1e21a2e6
--- /dev/null
+++ b/src/plots/circle-packing/constant.ts
@@ -0,0 +1,31 @@
+import { CirclePackingOptions } from './types';
+
+/** 默认的源字段 */
+export const RAW_FIELDS = ['x', 'y', 'r', 'name', 'value', 'path', 'depth'];
+
+export const DEFAULT_OPTIONS: Partial = {
+ // 默认按照 name 字段对颜色进行分类
+ colorField: 'name',
+ autoFit: true,
+ pointStyle: {
+ lineWidth: 0,
+ stroke: '#fff',
+ },
+ legend: false,
+ hierarchyConfig: {
+ size: [1, 1] as [number, number], // width, height
+ padding: 0,
+ },
+ label: {
+ fields: ['name'],
+ layout: {
+ type: 'limit-in-shape',
+ },
+ },
+ tooltip: {
+ showMarkers: false,
+ showTitle: false,
+ },
+ // 默认不可以下钻
+ drilldown: { enabled: false },
+};
diff --git a/src/plots/circle-packing/index.ts b/src/plots/circle-packing/index.ts
new file mode 100644
index 0000000000..b137beb0ac
--- /dev/null
+++ b/src/plots/circle-packing/index.ts
@@ -0,0 +1,49 @@
+import { Plot } from '../../core/plot';
+import { Adaptor } from '../../core/adaptor';
+import { adaptor } from './adaptor';
+import { DEFAULT_OPTIONS } from './constant';
+import { CirclePackingOptions } from './types';
+import './interactions';
+
+export type { CirclePackingOptions };
+
+/**
+ * CirclePacking
+ * @usage hierarchy, proportions
+ */
+export class CirclePacking extends Plot {
+ /**
+ * 获取 面积图 默认配置项
+ * 供外部使用
+ */
+ static getDefaultOptions(): Partial {
+ return DEFAULT_OPTIONS;
+ }
+ /** 图表类型 */
+ public type: string = 'circle-packing';
+
+ protected getDefaultOptions() {
+ return CirclePacking.getDefaultOptions();
+ }
+
+ /**
+ * 获取适配器
+ */
+ protected getSchemaAdaptor(): Adaptor {
+ return adaptor;
+ }
+
+ /**
+ * 覆写父类的方法
+ */
+ protected triggerResize() {
+ if (!this.chart.destroyed) {
+ // 首先自适应容器的宽高
+ this.chart.forceFit(); // g2 内部执行 changeSize,changeSize 中执行 render(true)
+ this.chart.clear();
+ this.execAdaptor(); // 核心:宽高更新之后计算padding
+ // 渲染
+ this.chart.render(true);
+ }
+ }
+}
diff --git a/src/plots/circle-packing/interactions/index.ts b/src/plots/circle-packing/interactions/index.ts
new file mode 100644
index 0000000000..ac242cd245
--- /dev/null
+++ b/src/plots/circle-packing/interactions/index.ts
@@ -0,0 +1,2 @@
+/** 引入 drill-down 交互 */
+import '../../../interactions/drill-down';
diff --git a/src/plots/circle-packing/types.ts b/src/plots/circle-packing/types.ts
new file mode 100644
index 0000000000..ae59da30b2
--- /dev/null
+++ b/src/plots/circle-packing/types.ts
@@ -0,0 +1,35 @@
+import { ColorAttr, Options, SizeAttr, StyleAttr } from '../../types';
+import { DrillDownCfg } from '../../types/drill-down';
+import { HierarchyOption } from '../../utils/hierarchy/types';
+
+export interface CirclePackingOptions extends Omit {
+ /** 数据字段 */
+ readonly data?: Record;
+ /** 层级布局配置 */
+ readonly hierarchyConfig?: Omit;
+
+ /** 颜色字段 */
+ readonly colorField?: string;
+
+ /** 颜色配置 */
+ readonly color?: ColorAttr;
+
+ /** 大小字段 */
+ readonly sizeField?: string;
+
+ /** 源字段 */
+ readonly rawFields?: string[];
+
+ // 暂不提供自定义 size,内部计算
+ // readonly size?: SizeAttr;
+
+ // 暂不提供 shape 配置,默认:circle.
+ // readonly shape?: string;
+
+ /** 图形样式 */
+ readonly pointStyle?: StyleAttr;
+
+ // 交互
+ /** 下钻交互 */
+ readonly drilldown?: DrillDownCfg;
+}
diff --git a/src/plots/circle-packing/utils.ts b/src/plots/circle-packing/utils.ts
new file mode 100644
index 0000000000..72d2726d3b
--- /dev/null
+++ b/src/plots/circle-packing/utils.ts
@@ -0,0 +1,109 @@
+import { Types } from '@antv/g2';
+import { pack } from '../../utils/hierarchy/pack';
+import { deepAssign, pick } from '../../utils';
+import { HIERARCHY_DATA_TRANSFORM_PARAMS } from '../../interactions/actions/drill-down';
+import { normalPadding } from '../../utils/padding';
+import { CirclePackingOptions } from './types';
+
+interface TransformDataOptions {
+ data: CirclePackingOptions['data'];
+ rawFields: CirclePackingOptions['rawFields'];
+ enableDrillDown: boolean;
+ hierarchyConfig: CirclePackingOptions['hierarchyConfig'];
+}
+
+/**
+ * circle-packing 数据转换
+ * @param options
+ */
+export function transformData(options: TransformDataOptions) {
+ const { data, hierarchyConfig, rawFields = [], enableDrillDown } = options;
+
+ const nodes = pack(data, {
+ ...hierarchyConfig,
+ field: 'value',
+ as: ['x', 'y', 'r'],
+ });
+
+ const result = [];
+ nodes.forEach((node) => {
+ let path = node.data.name;
+ let ancestorNode = { ...node };
+ while (ancestorNode.depth > 1) {
+ path = `${ancestorNode.parent.data?.name} / ${path}`;
+ ancestorNode = ancestorNode.parent;
+ }
+
+ // 开启下钻,仅加载 depth <= 2 的数据 (加载两层)
+ if (enableDrillDown && node.depth > 2) {
+ return null;
+ }
+
+ const nodeInfo = deepAssign({}, node.data, {
+ ...pick(node.data, rawFields),
+ path,
+ // 以下字段,必备: x, y, r, name, depth, height
+ ...node,
+ });
+
+ nodeInfo.ext = hierarchyConfig;
+ nodeInfo[HIERARCHY_DATA_TRANSFORM_PARAMS] = { hierarchyConfig, rawFields, enableDrillDown };
+
+ result.push(nodeInfo);
+ });
+
+ return result;
+}
+
+/**
+ * 根据图表的 padding 和 appendPadding 计算出图表的最终 padding
+ * @param array
+ */
+export function resolveAllPadding(paddings: Types.ViewPadding[]) {
+ // 先把数组里的 padding 全部转换成 normal
+ const normalPaddings = paddings.map((item) => normalPadding(item));
+ let finalPadding = [0, 0, 0, 0];
+ if (normalPaddings.length > 0) {
+ finalPadding = finalPadding.map((item, index) => {
+ // 有几个 padding 数组就遍历几次,累加
+ normalPaddings.forEach((d, i) => {
+ item += normalPaddings[i][index];
+ });
+ return item;
+ });
+ }
+ return finalPadding;
+}
+
+/**
+ * 根据传入的 padding 和 现有的 画布大小, 输出针对圆形视图布局需要的 finalPadding 以及 finalSize
+ * @param params
+ */
+export function resolvePaddingForCircle(
+ padding: Types.ViewPadding,
+ appendPadding: Types.ViewAppendPadding,
+ containerSize: { width: number; height: number }
+) {
+ const tempPadding = resolveAllPadding([padding, appendPadding]);
+ const [top, right, bottom, left] = tempPadding; // 没设定,默认是 [0, 0, 0, 0]
+ const { width, height } = containerSize;
+
+ // 有了 tempPadding 介入以后,计算出coordinate范围宽高的最小值 minSize = circle-packing的直径
+ const wSize = width - (left + right);
+ const hSize = height - (top + bottom);
+ const minSize = Math.min(wSize, hSize); // circle-packing的直径
+
+ // 得到居中后各方向剩余的 padding
+ const restWidthPadding = (wSize - minSize) / 2;
+ const restHeightPadding = (hSize - minSize) / 2;
+
+ const finalTop = top + restHeightPadding;
+ const finalRight = right + restWidthPadding;
+ const finalBottom = bottom + restHeightPadding;
+ const finalLeft = left + restWidthPadding;
+
+ const finalPadding = [finalTop, finalRight, finalBottom, finalLeft];
+ const finalSize = minSize < 0 ? 0 : minSize; // 防止为负数
+
+ return { finalPadding, finalSize };
+}
diff --git a/src/utils/hierarchy/pack.ts b/src/utils/hierarchy/pack.ts
new file mode 100644
index 0000000000..b561872e28
--- /dev/null
+++ b/src/utils/hierarchy/pack.ts
@@ -0,0 +1,49 @@
+import * as d3Hierarchy from 'd3-hierarchy';
+import { assign, isArray } from '@antv/util';
+import { getField, getAllNodes } from './util';
+import { HierarchyOption } from './types';
+
+type Options = Omit & { as?: [string, string, string] };
+
+const DEFAULT_OPTIONS: Options = {
+ field: 'value',
+ as: ['x', 'y', 'r'],
+ // 默认降序
+ sort: (a, b) => b.value - a.value,
+};
+
+export function pack(data: any, options: Options): any[] {
+ options = assign({} as Options, DEFAULT_OPTIONS, options);
+ const as = options.as;
+ if (!isArray(as) || as.length !== 3) {
+ throw new TypeError('Invalid as: it must be an array with 3 strings (e.g. [ "x", "y", "r" ])!');
+ }
+
+ let field;
+ try {
+ field = getField(options);
+ } catch (e) {
+ console.warn(e);
+ }
+
+ const packLayout = (data) =>
+ d3Hierarchy.pack().size(options.size).padding(options.padding)(
+ d3Hierarchy
+ .hierarchy(data)
+ .sum((d) => d[field])
+ .sort(options.sort)
+ );
+
+ const root = packLayout(data);
+
+ const x = as[0];
+ const y = as[1];
+ const r = as[2];
+ root.each((node) => {
+ node[x] = node.x;
+ node[y] = node.y;
+ node[r] = node.r;
+ });
+
+ return getAllNodes(root);
+}