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; }