diff --git a/__tests__/unit/plots/column/index-spec.ts b/__tests__/unit/plots/column/index-spec.ts
index 1b756a5db0..633a4eaae8 100644
--- a/__tests__/unit/plots/column/index-spec.ts
+++ b/__tests__/unit/plots/column/index-spec.ts
@@ -437,6 +437,23 @@ describe('column', () => {
plot.destroy();
});
+ it('custom shape', () => {
+ const plot = new Column(createDiv(), {
+ data: salesByArea,
+ xField: 'area',
+ yField: 'sales',
+ color: 'red',
+ });
+
+ plot.render();
+ expect(plot.chart.geometries[0].elements[0].shape.attr('fill')).toBe('red');
+
+ plot.update({ shape: 'hollow-rect' });
+ expect(plot.chart.geometries[0].elements[0].shape.attr('stroke')).toBe('red');
+
+ plot.destroy();
+ });
+
it('defaultOptions 保持从 constants 中获取', () => {
expect(Column.getDefaultOptions()).toEqual(DEFAULT_OPTIONS);
});
diff --git a/docs/api/plots/column.en.md b/docs/api/plots/column.en.md
index 5ffa3241fc..39881ce272 100644
--- a/docs/api/plots/column.en.md
+++ b/docs/api/plots/column.en.md
@@ -81,6 +81,12 @@ Width ratio of column [0-1].
The spacing between columns in a grouping [0-1] applies only to grouping columns.
+#### shape
+
+**可选** _string_
+
+内置 shape 类型有:`hollow-rect`, `tick`; 此外,还可以搭配 [`registerShape`](https://g2.antv.vision/en/docs/api/advanced/register-shape) 进行自定义使用.
+
#### state
**optional** _object_
diff --git a/docs/api/plots/column.zh.md b/docs/api/plots/column.zh.md
index ebee783207..d3ffc05fde 100644
--- a/docs/api/plots/column.zh.md
+++ b/docs/api/plots/column.zh.md
@@ -83,6 +83,14 @@ order: 2
分组中柱子之间的间距 [0-1],仅对分组柱状图适用。
+#### shape
+
+**可选** _string_
+
+内置 shape 类型有:`hollow-rect`, `tick`; 此外,还可以搭配 [`registerShape`](https://g2.antv.vision/zh/docs/api/advanced/register-shape) 进行自定义使用.
+
+[Demo](/zh/examples/column/basic#custom-shape)
+
#### state
**可选** _object_
diff --git a/examples/column/basic/demo/custom-shape.ts b/examples/column/basic/demo/custom-shape.ts
new file mode 100644
index 0000000000..cd408b7f8d
--- /dev/null
+++ b/examples/column/basic/demo/custom-shape.ts
@@ -0,0 +1,199 @@
+import { G2, Column } from '@antv/g2plot';
+
+const LATEST_FLAG = 'LATEST_FLAG';
+
+function getIntervalRectPath(points, isClosed = true) {
+ const path = [];
+ const firstPoint = points[0];
+ path.push(['M', firstPoint.x, firstPoint.y]);
+ for (let i = 1, len = points.length; i < len; i++) {
+ path.push(['L', points[i].x, points[i].y]);
+ }
+ // 对于 shape="line" path 不应该闭合,否则会造成 lineCap 绘图属性失效
+ if (isClosed) {
+ path.push(['L', firstPoint.x, firstPoint.y]); // 需要闭合
+ path.push(['z']);
+ }
+ return path;
+}
+
+G2.registerAnimation('label-update', (element, animateCfg, cfg) => {
+ const startX = element.attr('x');
+ const startY = element.attr('y');
+ // @ts-ignore
+ const finalX = cfg.toAttrs.x;
+ // @ts-ignore
+ const finalY = cfg.toAttrs.y;
+
+ const labelContent = element.attr('text');
+ // @ts-ignore
+ const finalContent = cfg.toAttrs.text;
+
+ const distanceX = finalX - startX;
+ const distanceY = finalY - startY;
+ const numberDiff = +finalContent - +labelContent;
+
+ element.animate((ratio) => {
+ const positionX = startX + distanceX * ratio;
+ const positionY = startY + distanceY * ratio;
+ const value = +labelContent + numberDiff * ratio;
+
+ return {
+ x: positionX,
+ y: positionY,
+ text: value.toFixed(0),
+ };
+ }, animateCfg);
+});
+
+G2.registerShape('interval', 'blink-interval', {
+ draw(cfg, container) {
+ const group = container.addGroup();
+ const path = this.parsePath(getIntervalRectPath(cfg.points));
+ const { color, style = {}, defaultStyle } = cfg;
+ const fillColor = color || style.fill || defaultStyle.fill;
+
+ const height = path[1][2] - path[0][2];
+ const width = path[3][1] - path[0][1];
+ const x = path[0][1];
+ const y = path[0][2];
+ group.addShape('path', {
+ attrs: {
+ ...style,
+ path,
+ fill: fillColor,
+ x,
+ y,
+ width,
+ height,
+ },
+ name: 'interval',
+ });
+
+ const data = cfg.data;
+ if (data[LATEST_FLAG]) {
+ group.addShape('rect', {
+ attrs: {
+ x,
+ y,
+ width,
+ height,
+ fill: `l(90) 0:${fillColor} 1:rgba(255,255,255,0.23)`,
+ },
+ name: 'blink-interval',
+ });
+ }
+
+ return group;
+ },
+});
+
+G2.registerAnimation('appear-interval', (shape, animateCfg, cfg) => {
+ const growInY = G2.getAnimation('scale-in-y');
+ growInY(shape, animateCfg, cfg);
+
+ const blinkShape = shape.getParent().findAllByName('blink-interval')[0];
+ if (blinkShape) {
+ const { height } = blinkShape.attr();
+ blinkShape.attr('height', 0);
+ blinkShape.animate(
+ {
+ height: height,
+ },
+ {
+ duration: 1000,
+ easing: 'easeQuadOut',
+ repeat: true,
+ }
+ );
+ }
+});
+
+G2.registerAnimation('blink-interval', (element, animateCfg, cfg) => {
+ const container = element.getParent();
+
+ const shape = container.findAllByName('interval')[0];
+ const blinkShape = container.findAllByName('blink-interval')[0];
+
+ if (shape && cfg.toAttrs.path) {
+ shape.animate(cfg.toAttrs, animateCfg);
+ }
+
+ if (blinkShape) {
+ blinkShape.stopAnimate(true);
+ blinkShape.attr({ x: cfg.toAttrs.x, y: cfg.toAttrs.y });
+ blinkShape.attr({ height: 0, width: cfg.toAttrs.width });
+ blinkShape.animate(
+ {
+ height: cfg.toAttrs.height,
+ },
+ {
+ duration: 1000,
+ easing: 'easeQuadOut',
+ delay: 50,
+ repeat: true,
+ }
+ );
+ }
+});
+
+fetch('https://gw.alipayobjects.com/os/antfincdn/xXg6cUV0lV/column.json')
+ .then((data) => data.json())
+ .then((data) => {
+ const plot = new Column('container', {
+ data: data.map((d, idx) => ({ ...d, [LATEST_FLAG]: idx === data.length - 1 })),
+ xField: 'month',
+ yField: 'value',
+ appendPadding: [10],
+ label: {
+ // 可手动配置 label 数据标签位置
+ position: 'top', // 'top', 'bottom', 'middle',
+ offset: 4,
+ animate: {
+ update: {
+ animation: 'label-update',
+ duration: 300,
+ easing: 'easeLinear',
+ },
+ },
+ },
+ xAxis: {
+ label: {
+ autoHide: true,
+ autoRotate: false,
+ },
+ },
+ meta: {
+ month: {
+ alias: '月份',
+ },
+ value: {
+ alias: '销售额',
+ nice: true,
+ },
+ },
+ shape: 'blink-interval',
+ animation: {
+ appear: {
+ animation: 'appear-interval',
+ },
+ update: {
+ animation: 'blink-interval',
+ },
+ },
+ });
+
+ plot.render();
+ let timer = 0;
+ const interval = setInterval(() => {
+ const chartData = plot.chart.getData();
+ plot.chart.changeData(
+ chartData.map((d) => ({ ...d, value: d[LATEST_FLAG] ? d.value + ((Math.random() * 250) | 0) : d.value }))
+ );
+
+ timer++;
+ if (timer > 500) {
+ clearInterval(interval);
+ }
+ }, 2000);
+ });
diff --git a/examples/column/basic/demo/meta.json b/examples/column/basic/demo/meta.json
index 04d5937521..5f1065ecd8 100644
--- a/examples/column/basic/demo/meta.json
+++ b/examples/column/basic/demo/meta.json
@@ -67,6 +67,15 @@
"en": "Basic column plot with region annotation"
},
"screenshot": "https://gw.alipayobjects.com/zos/antfincdn/ceFSs9FuNn/2922d5e4-df5f-4512-8f8f-6f2ec258c7b8.png"
+ },
+ {
+ "filename": "custom-shape.ts",
+ "title": {
+ "zh": "自定义柱状图图形元素展示",
+ "en": "Custom column shape"
+ },
+ "new": true,
+ "screenshot": ""
}
]
}
diff --git a/src/plots/column/adaptor.ts b/src/plots/column/adaptor.ts
index 7eb487f1ce..5754cde27c 100644
--- a/src/plots/column/adaptor.ts
+++ b/src/plots/column/adaptor.ts
@@ -67,6 +67,7 @@ function geometry(params: Params): Params {
seriesField,
groupField,
tooltip,
+ shape,
} = options;
const percentData =
@@ -110,6 +111,7 @@ function geometry(params: Params): Params {
widthRatio: columnWidthRatio,
tooltip: tooltipOptions,
interval: {
+ shape,
style: columnStyle,
color,
},
diff --git a/src/plots/column/types.ts b/src/plots/column/types.ts
index 8a9c8f49c8..0cfb3a5c19 100644
--- a/src/plots/column/types.ts
+++ b/src/plots/column/types.ts
@@ -40,6 +40,11 @@ export interface ColumnOptions
/** 分组字段,优先级高于 seriesField , isGroup: true 时会根据 groupField 进行分组。*/
readonly groupField?: string;
+ // 自定义相关
+ /** 自定义柱状图 interval 图形元素展示形状 */
+ readonly shape?: string;
+
// 图表交互
+ /** 开启下钻交互,以及进行下钻交互的配置 */
readonly brush?: BrushCfg;
}