diff --git a/__tests__/data/sales.ts b/__tests__/data/sales.ts index faea682969..8608e9f3ef 100644 --- a/__tests__/data/sales.ts +++ b/__tests__/data/sales.ts @@ -6,3 +6,96 @@ export const salesByArea = [ { area: '西北', sales: 815039.5959999998 }, { area: '西南', sales: 1303124.508000002 }, ]; + +export const subSalesByArea = [ + { + area: '东北', + series: '消费者', + sales: 1323985.6069999991, + }, + { + area: '东北', + series: '小型企业', + sales: 522739.0349999995, + }, + { + area: '东北', + series: '公司', + sales: 834842.827, + }, + { + area: '中南', + series: '消费者', + sales: 2057936.7620000008, + }, + { + area: '中南', + series: '小型企业', + sales: 743813.0069999992, + }, + { + area: '中南', + series: '公司', + sales: 1335665.3239999984, + }, + { + area: '华东', + series: '消费者', + sales: 2287358.261999998, + }, + { + area: '华东', + series: '小型企业', + sales: 942432.3720000006, + }, + { + area: '华东', + series: '公司', + sales: 1454715.807999998, + }, + { + area: '华北', + series: '消费者', + sales: 1220430.5610000012, + }, + { + area: '华北', + series: '小型企业', + sales: 422100.9870000001, + }, + { + area: '华北', + series: '公司', + sales: 804769.4689999995, + }, + { + area: '西北', + series: '消费者', + sales: 458058.1039999998, + }, + { + area: '西北', + series: '小型企业', + sales: 103523.308, + }, + { + area: '西北', + series: '公司', + sales: 253458.1840000001, + }, + { + area: '西南', + series: '消费者', + sales: 677302.8919999995, + }, + { + area: '西南', + series: '小型企业', + sales: 156479.9319999999, + }, + { + area: '西南', + series: '公司', + sales: 469341.684, + }, +]; diff --git a/__tests__/unit/plots/column/index-spec.ts b/__tests__/unit/plots/column/index-spec.ts index ddbab09235..56c735a876 100644 --- a/__tests__/unit/plots/column/index-spec.ts +++ b/__tests__/unit/plots/column/index-spec.ts @@ -1,10 +1,10 @@ import { Column } from '../../../../src'; -import { salesByArea } from '../../../data/sales'; +import { salesByArea, subSalesByArea } from '../../../data/sales'; import { createDiv } from '../../../utils/dom'; describe('column', () => { it('x*y', () => { - const column = new Column(createDiv(), { + const column = new Column(createDiv('x*y'), { width: 400, height: 300, data: salesByArea, @@ -28,7 +28,7 @@ describe('column', () => { }); it('x*y*color', () => { - const column = new Column(createDiv(), { + const column = new Column(createDiv('x*y*color'), { width: 400, height: 300, data: salesByArea, @@ -48,7 +48,7 @@ describe('column', () => { it('x*y*color with color', () => { const palette = ['red', 'yellow', 'green']; - const column = new Column(createDiv(), { + const column = new Column(createDiv('x*y*color with color'), { width: 400, height: 300, data: salesByArea, @@ -71,4 +71,45 @@ describe('column', () => { expect(color).toBe(palette[index % palette.length]); }); }); + + it('grouped column', () => { + const column = new Column(createDiv('grouped column'), { + width: 400, + height: 300, + data: subSalesByArea, + xField: 'area', + yField: 'sales', + colorField: 'series', + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + expect(geometry.getAdjust('dodge')).toMatchObject({ + xField: 'area', + yField: 'sales', + }); + expect(geometry.getAdjust('stack')).toBeUndefined(); + }); + + it('stacked column', () => { + const column = new Column(createDiv('stacked column'), { + width: 400, + height: 300, + data: subSalesByArea, + xField: 'area', + yField: 'sales', + colorField: 'series', + isStack: true, + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + expect(geometry.getAdjust('dodge')).toBeUndefined(); + expect(geometry.getAdjust('stack')).toMatchObject({ + xField: 'area', + yField: 'sales', + }); + }); }); diff --git a/__tests__/unit/plots/column/label-spec.ts b/__tests__/unit/plots/column/label-spec.ts index 3678410d91..f8c7f5f000 100644 --- a/__tests__/unit/plots/column/label-spec.ts +++ b/__tests__/unit/plots/column/label-spec.ts @@ -1,10 +1,10 @@ import { Column } from '../../../../src'; -import { salesByArea } from '../../../data/sales'; +import { salesByArea, subSalesByArea } from '../../../data/sales'; import { createDiv } from '../../../utils/dom'; describe('column label', () => { - it('position: top', () => { - const column = new Column(createDiv(), { + it('position top', () => { + const column = new Column(createDiv('position top'), { width: 400, height: 300, data: salesByArea, @@ -36,8 +36,8 @@ describe('column label', () => { }); }); - it('label position middle', () => { - const column = new Column(createDiv(), { + it('position middle', () => { + const column = new Column(createDiv('position middle'), { width: 400, height: 300, data: salesByArea, @@ -62,8 +62,8 @@ describe('column label', () => { expect(geometry.labelOption.cfg).toEqual({ position: 'middle' }); }); - it('label position bottom', () => { - const column = new Column(createDiv(), { + it('position bottom', () => { + const column = new Column(createDiv('position bottom'), { width: 400, height: 300, data: salesByArea, @@ -87,4 +87,109 @@ describe('column label', () => { // @ts-ignore expect(geometry.labelOption.cfg).toEqual({ position: 'bottom' }); }); + + it('group column position top', () => { + const column = new Column(createDiv('group column position top'), { + width: 400, + height: 300, + data: subSalesByArea, + xField: 'area', + yField: 'sales', + colorField: 'series', + meta: { + sales: { + nice: true, + formatter: (v) => `${Math.floor(v / 10000)}万`, + }, + }, + label: { + position: 'top', + }, + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + const labelGroups = geometry.labelsContainer.getChildren(); + + // @ts-ignore + expect(geometry.labelOption.cfg).toEqual({ + position: 'top', + }); + expect(labelGroups).toHaveLength(subSalesByArea.length); + labelGroups.forEach((label) => { + const origin = label.get('origin')._origin; + expect(label.get('children')[0].attr('text')).toBe(`${Math.floor(origin.sales / 10000)}万`); + }); + }); + + it('group column position middle', () => { + const column = new Column(createDiv('group column position middle'), { + width: 400, + height: 300, + data: subSalesByArea, + xField: 'area', + yField: 'sales', + colorField: 'series', + meta: { + sales: { + nice: true, + formatter: (v) => `${Math.floor(v / 10000)}万`, + }, + }, + label: { + position: 'middle', + }, + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + const labelGroups = geometry.labelsContainer.getChildren(); + + // @ts-ignore + expect(geometry.labelOption.cfg).toEqual({ + position: 'middle', + }); + expect(labelGroups).toHaveLength(subSalesByArea.length); + labelGroups.forEach((label) => { + const origin = label.get('origin')._origin; + expect(label.get('children')[0].attr('text')).toBe(`${Math.floor(origin.sales / 10000)}万`); + }); + }); + + it('group column position bottom', () => { + const column = new Column(createDiv('group column position bottom'), { + width: 400, + height: 300, + data: subSalesByArea, + xField: 'area', + yField: 'sales', + colorField: 'series', + meta: { + sales: { + nice: true, + formatter: (v) => `${Math.floor(v / 10000)}万`, + }, + }, + label: { + position: 'bottom', + }, + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + const labelGroups = geometry.labelsContainer.getChildren(); + + // @ts-ignore + expect(geometry.labelOption.cfg).toEqual({ + position: 'bottom', + }); + expect(labelGroups).toHaveLength(subSalesByArea.length); + labelGroups.forEach((label) => { + const origin = label.get('origin')._origin; + expect(label.get('children')[0].attr('text')).toBe(`${Math.floor(origin.sales / 10000)}万`); + }); + }); }); diff --git a/__tests__/utils/dom.ts b/__tests__/utils/dom.ts index 1f10767455..0b1b406129 100644 --- a/__tests__/utils/dom.ts +++ b/__tests__/utils/dom.ts @@ -1,10 +1,16 @@ /** * 创建一个 div 节点,并放到 container,默认放到 body 上 + * @param title * @param container */ -export function createDiv(container: HTMLElement = document.body): HTMLElement { +export function createDiv(title: string = '', container: HTMLElement = document.body): HTMLElement { const div = document.createElement('div'); + if (title) { + const titleDiv = document.createElement('div').appendChild(document.createTextNode(title)); + container.appendChild(titleDiv); + } + container.appendChild(div); return div; diff --git a/src/plots/column/adaptor.ts b/src/plots/column/adaptor.ts index fe2259751a..dd9a5a5a7d 100644 --- a/src/plots/column/adaptor.ts +++ b/src/plots/column/adaptor.ts @@ -2,6 +2,7 @@ import { Geometry, Chart } from '@antv/g2'; import { deepMix, isFunction } from '@antv/util'; import { Params } from '../../core/adaptor'; import { findGeometry } from '../../common/helper'; +import { tooltip, interaction, animation, theme } from '../../common/adaptor'; import { flow, pick } from '../../utils'; import { ColumnOptions } from './types'; import { AXIS_META_CONFIG_KEYS } from '../../constant'; @@ -12,7 +13,7 @@ import { AXIS_META_CONFIG_KEYS } from '../../constant'; */ function field(params: Params): Params { const { chart, options } = params; - const { data, xField, yField, colorField, color } = options; + const { data, xField, yField, colorField, color, isStack } = options; chart.data(data); const geometry = chart.interval().position(`${xField}*${yField}`); @@ -21,6 +22,10 @@ function field(params: Params): Params { geometry.color(colorField, color); } + if (colorField && ![xField, yField].includes(colorField)) { + geometry.adjust(isStack ? 'stack' : 'dodge'); + } + return params; } @@ -129,5 +134,5 @@ function label(params: Params): Params { * @param params */ export function adaptor(params: Params) { - return flow(field, meta, axis, legend, style, label)(params); + return flow(field, meta, axis, legend, tooltip, theme, style, label, interaction, animation)(params); } diff --git a/src/plots/column/types.ts b/src/plots/column/types.ts index 08f5a39c5c..113237a491 100644 --- a/src/plots/column/types.ts +++ b/src/plots/column/types.ts @@ -3,11 +3,17 @@ import { ShapeStyle } from '../../types/style'; export interface ColumnOptions extends Options { /** x 轴字段 */ - readonly xField?: string; + readonly xField: string; /** y 轴字段 */ - readonly yField?: string; + readonly yField: string; /** 颜色字段,可选 */ readonly colorField?: string; + /** 是否 堆积柱状图, 默认 分组柱状图 */ + readonly isStack?: boolean; + /** 柱子宽度占比 [0-1] */ + readonly marginRatio?: number; + /** 分组或堆叠内部的间距,像素值 */ + readonly innerPadding?: number; /** 柱子样式配置,可选 */ readonly columnStyle?: ShapeStyle | ((x: any, y: any, color?: any) => ShapeStyle); }