diff --git a/__tests__/data/box.ts b/__tests__/data/box.ts new file mode 100644 index 00000000000..b9b9bb3679d --- /dev/null +++ b/__tests__/data/box.ts @@ -0,0 +1,10 @@ +export const boxData = [ + { x: 'Oceania', low: 1, q1: 9, median: 16, q3: 22, high: 24 }, + { x: 'East Europe', low: 1, q1: 5, median: 8, q3: 12, high: 16 }, + { x: 'Australia', low: 1, q1: 8, median: 12, q3: 19, high: 26 }, + { x: 'South America', low: 2, q1: 8, median: 12, q3: 21, high: 28 }, + { x: 'North Africa', low: 1, q1: 8, median: 14, q3: 18, high: 24 }, + { x: 'North America', low: 3, q1: 10, median: 17, q3: 28, high: 30 }, + { x: 'West Europe', low: 1, q1: 7, median: 10, q3: 17, high: 22 }, + { x: 'West Africa', low: 1, q1: 6, median: 8, q3: 13, high: 16 }, +]; diff --git a/__tests__/unit/plots/box/index-spec.ts b/__tests__/unit/plots/box/index-spec.ts new file mode 100644 index 00000000000..13c92085d6f --- /dev/null +++ b/__tests__/unit/plots/box/index-spec.ts @@ -0,0 +1,33 @@ +import { Box } from '../../../../src'; +import { boxData } from '../../../data/box'; +import { createDiv } from '../../../utils/dom'; + +const default_range_field = '@@__range'; + +describe('column', () => { + it('x*range range.min default as 0', () => { + const column = new Box(createDiv('x*range range.min default as 0'), { + width: 400, + height: 500, + data: boxData, + xField: 'x', + yField: ['low', 'q1', 'median', 'q3', 'high'], + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + const positionFields = geometry.getAttribute('position').getFields(); + + // 类型 + expect(geometry.type).toBe('schema'); + // 图形元素个数 + expect(column.chart.geometries[0].elements.length).toBe(boxData.length); + // x & range + expect(positionFields).toHaveLength(2); + + // range meta default min = 0 + // @ts-ignore + expect(geometry.scales[default_range_field].min).toBe(0); + }); +}); diff --git a/__tests__/unit/plots/box/style-spec.ts b/__tests__/unit/plots/box/style-spec.ts new file mode 100644 index 00000000000..eec1881bf7a --- /dev/null +++ b/__tests__/unit/plots/box/style-spec.ts @@ -0,0 +1,37 @@ +import { Box } from '../../../../src'; +import { boxData } from '../../../data/box'; +import { createDiv } from '../../../utils/dom'; + +describe('column style', () => { + it('style config', () => { + const column = new Box(createDiv('style config'), { + width: 400, + height: 500, + data: boxData, + xField: 'x', + yField: ['low', 'q1', 'median', 'q3', 'high'], + meta: { + sales: { + nice: true, + formatter: (v) => `${Math.floor(v / 10000)}万`, + }, + }, + boxStyle: { + stroke: 'black', + lineWidth: 2, + fill: '#1890FF', + }, + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + const elements = geometry.elements; + expect(elements[0].shape.attr('stroke')).toBe('black'); + expect(elements[0].shape.attr('lineWidth')).toBe(2); + expect(elements[0].shape.attr('fill')).toBe('#1890FF'); + }); + + // TODO + // it('style callback', () => {}); +}); diff --git a/src/index.ts b/src/index.ts index b0b9c88232d..cd54d11f658 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,3 +44,6 @@ export { Bar, BarOptions } from './plots/bar'; // 雷达图及类型定义 export { Radar, RadarOptions } from './plots/radar'; + +// 箱线图及类型定义 +export { Box, BoxOptions } from './plots/box'; diff --git a/src/plots/box/adaptor.ts b/src/plots/box/adaptor.ts new file mode 100644 index 00000000000..064e8bff8c6 --- /dev/null +++ b/src/plots/box/adaptor.ts @@ -0,0 +1,127 @@ +import { deepMix, isFunction, isNil } from '@antv/util'; +import DataSet from '@antv/data-set'; +import { Params } from '../../core/adaptor'; +import { findGeometry } from '../../common/helper'; +import { BoxOptions } from './types'; +import { flow, pick } from '../../utils'; +import { AXIS_META_CONFIG_KEYS } from '../../constant'; + +const RANGE = '@@__range'; + +/** + * 字段 + * @param params + */ +function field(params: Params): Params { + const { chart, options } = params; + const { xField, yField, data } = options; + const [low, q1, median, q3, high] = yField; + + const ds = new DataSet(); + const dv = ds.createView().source(data); + + // dataset 处理数据 + dv.transform({ + type: 'map', + callback: (obj) => { + obj[RANGE] = [obj[low], obj[q1], obj[median], obj[q3], obj[high]]; + return obj; + }, + }); + + chart.schema().position(`${xField}*${RANGE}`).shape('box'); + chart.data(dv.rows); + + return params; +} + +/** + * meta 配置 + * @param params + */ +function meta(params: Params): Params { + const { chart, options } = params; + const { meta, xAxis, yAxis, xField } = options; + + const scales = deepMix( + { + // 箱线图默认 range 从0 开始 + [RANGE]: { min: 0 }, + }, + meta, + { + [xField]: pick(xAxis, AXIS_META_CONFIG_KEYS), + [RANGE]: pick(yAxis, AXIS_META_CONFIG_KEYS), + } + ); + + chart.scale(scales); + + return params; +} + +/** + * axis 配置 + * @param params + */ +function axis(params: Params): Params { + const { chart, options } = params; + const { xAxis, yAxis, xField } = options; + + // 为 false 则是不显示轴 + if (xAxis === false) { + chart.axis(xField, false); + } else { + chart.axis(xField, xAxis); + } + + if (yAxis === false) { + chart.axis(RANGE, false); + } else { + chart.axis(RANGE, yAxis); + } + + 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; +// } + +/** + * 样式 + * @param params + */ +function style(params: Params): Params { + const { chart, options } = params; + const { xField, yField, boxStyle } = options; + + const geometry = findGeometry(chart, 'schema'); + if (boxStyle && geometry) { + if (isFunction(boxStyle)) { + // geometry.style(`${xField}*${yField}*${colorField}`, columnStyle); + } else { + geometry.style(boxStyle); + } + } + return params; +} + +/** + * 箱线图适配器 + * @param params + */ +export function adaptor(params: Params) { + return flow(field, meta, axis, style)(params); +} diff --git a/src/plots/box/index.ts b/src/plots/box/index.ts new file mode 100644 index 00000000000..6f315710a40 --- /dev/null +++ b/src/plots/box/index.ts @@ -0,0 +1,18 @@ +import { Plot } from '../../core/plot'; +import { Adaptor } from '../../core/adaptor'; +import { BoxOptions } from './types'; +import { adaptor } from './adaptor'; + +export { BoxOptions }; + +export class Box extends Plot { + /** 图表类型 */ + public type: string = 'box'; + + /** + * 获取直方图的适配器 + */ + protected getSchemaAdaptor(): Adaptor { + return adaptor; + } +} diff --git a/src/plots/box/types.ts b/src/plots/box/types.ts new file mode 100644 index 00000000000..bb3ab3a9df9 --- /dev/null +++ b/src/plots/box/types.ts @@ -0,0 +1,11 @@ +import { Options } from '../../types'; +import { ShapeStyle } from '../../types/style'; + +export interface BoxOptions extends Options { + /** x 轴字段 */ + readonly xField: string; + /** y 轴映射 box range [low, q1, median, q3, hight] 五个字段 */ + readonly yField: [string?, string?, string?, string?, string?]; + /** 柱子样式配置,可选 */ + readonly boxStyle?: ShapeStyle | ((x: any, y: any, color?: any) => ShapeStyle); +}