From 0b481d519bb5726681d7a8b0c33dd7d33c17d3fc Mon Sep 17 00:00:00 2001 From: hustcc Date: Wed, 22 Jul 2020 11:43:24 +0800 Subject: [PATCH 1/5] feat: line options (#1308) * feat(interaction): move interaction to common, add line interaction * feat(line): add animation options * feat(theme): add theme option for all * feat(line): add point options --- __tests__/unit/core/index-spec.ts | 33 +++++++++++++- __tests__/unit/plots/line/animation-spec.ts | 49 ++++++++++++++++++++ __tests__/unit/plots/line/point-spec.ts | 50 +++++++++++++++++++++ src/common/adaptor.ts | 49 ++++++++++++++++++++ src/index.ts | 3 ++ src/plots/line/adaptor.ts | 33 +++++++++++++- src/plots/line/types.ts | 13 +++--- src/plots/pie/adaptor.ts | 21 +-------- src/plots/tiny-line/adaptor.ts | 18 +++++++- src/plots/tiny-line/types.ts | 2 + src/types/animation.ts | 4 +- src/types/common.ts | 2 +- 12 files changed, 245 insertions(+), 32 deletions(-) create mode 100644 __tests__/unit/plots/line/animation-spec.ts create mode 100644 __tests__/unit/plots/line/point-spec.ts diff --git a/__tests__/unit/core/index-spec.ts b/__tests__/unit/core/index-spec.ts index bc6d30d43b..a46257538a 100644 --- a/__tests__/unit/core/index-spec.ts +++ b/__tests__/unit/core/index-spec.ts @@ -1,7 +1,11 @@ -import { Line } from '../../../src'; +import { Line, registerTheme } from '../../../src'; import { partySupport } from '../../data/party-support'; import { createDiv } from '../../utils/dom'; +registerTheme('new-theme', { + colors10: ['green'], +}); + describe('core', () => { it('autoFit', () => { const line = new Line(createDiv(), { @@ -46,4 +50,31 @@ describe('core', () => { expect(line.chart.localRefresh).toBe(false); }); + + it('theme', () => { + const line = new Line(createDiv(), { + width: 400, + height: 300, + appendPadding: 10, + data: partySupport.filter((o) => ['FF', 'Lab'].includes(o.type)), + xField: 'date', + yField: 'value', + seriesField: 'type', + theme: { + colors10: ['red'], + }, + }); + + line.render(); + + expect(line.chart.getTheme().colors10).toEqual(['red']); + expect(line.chart.getTheme().defaultColor).toBe('#5B8FF9'); + + line.update({ + ...line.options, + theme: 'new-theme', + }); + + expect(line.chart.getTheme().colors10).toEqual(['green']); + }); }); diff --git a/__tests__/unit/plots/line/animation-spec.ts b/__tests__/unit/plots/line/animation-spec.ts new file mode 100644 index 0000000000..b0a971c64f --- /dev/null +++ b/__tests__/unit/plots/line/animation-spec.ts @@ -0,0 +1,49 @@ +import { Line } from '../../../../src'; +import { partySupport } from '../../../data/party-support'; +import { createDiv } from '../../../utils/dom'; + +describe('line', () => { + it('x*y with animation', () => { + const line = new Line(createDiv(), { + width: 400, + height: 300, + data: partySupport.filter((o) => ['FF'].includes(o.type)), + xField: 'date', + yField: 'value', + appendPadding: 10, + smooth: true, + animation: { + enter: { + animation: 'fade-in', + }, + leave: { + animation: 'fade-out', + }, + }, + }); + + line.render(); + + // 追加默认的动画配置 + expect(line.chart.geometries[0].animateOption).toEqual({ + appear: { + duration: 450, + easing: 'easeQuadOut', + }, + update: { + duration: 400, + easing: 'easeQuadInOut', + }, + enter: { + duration: 400, + easing: 'easeQuadInOut', + animation: 'fade-in', + }, + leave: { + duration: 350, + easing: 'easeQuadIn', + animation: 'fade-out', + }, + }); + }); +}); diff --git a/__tests__/unit/plots/line/point-spec.ts b/__tests__/unit/plots/line/point-spec.ts new file mode 100644 index 0000000000..19f61267a4 --- /dev/null +++ b/__tests__/unit/plots/line/point-spec.ts @@ -0,0 +1,50 @@ +import { Line } from '../../../../src'; +import { partySupport } from '../../../data/party-support'; +import { createDiv } from '../../../utils/dom'; + +describe('line', () => { + it('x*y*color point', () => { + const line = new Line(createDiv(), { + width: 400, + height: 300, + data: partySupport.filter((o) => ['FF', 'Lab'].includes(o.type)), + xField: 'date', + yField: 'value', + seriesField: 'type', + appendPadding: 10, + }); + + line.render(); + expect(line.chart.geometries.length).toBe(1); + + let xValue; + let yValue; + let colorValue; + line.update({ + ...line.options, + point: { + size: 2, + shape: 'circle', + style: (x: string, y: number, color: string) => { + xValue = x; + yValue = y; + colorValue = color; + return { + fill: color === 'FF' ? 'red' : 'blue', + }; + }, + }, + }); + expect(line.chart.geometries.length).toBe(2); + expect(xValue).toBe('25/01/2018'); + expect(yValue).toBe(400); + expect(colorValue).toBe('Lab'); + + const point = line.chart.geometries[1]; + expect(point.shapeType).toBe('point'); + // @ts-ignore + expect(point.attributeOption.size.values).toEqual([2]); + // @ts-ignore + // expect(point.attributeOption.shape.values).toEqual(['circle']); + }); +}); diff --git a/src/common/adaptor.ts b/src/common/adaptor.ts index 35f97b7223..319caf6f02 100644 --- a/src/common/adaptor.ts +++ b/src/common/adaptor.ts @@ -1,8 +1,11 @@ /** * @file 通用的一些 adaptor */ +import { Geometry } from '@antv/g2'; +import { each } from '@antv/util'; import { Params } from '../core/adaptor'; import { Options } from '../types'; +import { Interaction } from '../types/interaction'; /** * 通用 tooltip 配置 @@ -18,3 +21,49 @@ export function tooltip(params: Params): Params { return params; } + +/** + * Interaction 配置 + * @param params + */ +export function interaction(params: Params): Params { + const { chart, options } = params; + const { interactions } = options; + + each(interactions, (i: Interaction) => { + chart.interaction(i.name, i.cfg || {}); + }); + + return params; +} + +/** + * 动画 + * @param params + */ +export function animation(params: Params): Params { + const { chart, options } = params; + const { animation } = options; + + // 所有的 Geometry 都使用同一动画(各个图形如有区别,自行覆盖) + each(chart.geometries, (g: Geometry) => { + g.animate(animation); + }); + + return params; +} + +/** + * 设置全局主题配置 + * @param params + */ +export function theme(params: Params): Params { + const { chart, options } = params; + const { theme } = options; + + // 存在主题才设置主题 + if (theme) { + chart.theme(theme); + } + return params; +} diff --git a/src/index.ts b/src/index.ts index 5ce9654c78..fb583a0152 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,8 @@ export const version = '2.0.0'; +// G2 自定义能力透出 +export { registerTheme } from '@antv/g2'; + // 类型定义导出 export * from './types'; diff --git a/src/plots/line/adaptor.ts b/src/plots/line/adaptor.ts index 9ed772da37..f407f2eb21 100644 --- a/src/plots/line/adaptor.ts +++ b/src/plots/line/adaptor.ts @@ -1,7 +1,7 @@ import { Geometry } from '@antv/g2'; import { deepMix, isFunction } from '@antv/util'; import { Params } from '../../core/adaptor'; -import { tooltip } from '../../common/adaptor'; +import { tooltip, interaction, animation, theme } from '../../common/adaptor'; import { flow, pick } from '../../utils'; import { LineOptions } from './types'; @@ -141,6 +141,35 @@ function label(params: Params): Params { return params; } +/** + * point 辅助点的配置处理 + * @param params + */ +function point(params: Params): Params { + const { chart, options } = params; + const { point, seriesField, xField, yField } = options; + + if (point) { + const { shape, size, style } = point; + const pointGeometry = chart.point().position(`${xField}*${yField}`).size(size); + + // shape + if (isFunction(shape)) { + pointGeometry.shape(`${xField}*${yField}*${seriesField}`, shape); + } else { + pointGeometry.shape(shape); + } + + // style + if (isFunction(style)) { + pointGeometry.style(`${xField}*${yField}*${seriesField}`, style); + } else { + pointGeometry.style(style); + } + } + return params; +} + /** * 折线图适配器 * @param chart @@ -148,5 +177,5 @@ function label(params: Params): Params { */ export function adaptor(params: Params) { // flow 的方式处理所有的配置到 G2 API - flow(field, meta, axis, legend, tooltip, style, shape, label)(params); + flow(field, meta, point, theme, axis, legend, tooltip, style, shape, label, interaction, animation)(params); } diff --git a/src/plots/line/types.ts b/src/plots/line/types.ts index 40f90ada54..65448aaa8b 100644 --- a/src/plots/line/types.ts +++ b/src/plots/line/types.ts @@ -12,14 +12,15 @@ export interface LineOptions extends Options { readonly smooth?: boolean; /** 是否连接空数据 */ readonly connectNulls?: boolean; - /** 折线 extra 图形样式 */ + /** 折线图形样式 */ readonly lineStyle?: ShapeStyle | ((x?: any, y?: any, color?: any) => ShapeStyle); /** 折线数据点图形样式 */ readonly point?: { - visible?: boolean; - shape?: ShapeStyle; - size?: number; - color?: string; - style?: ShapeStyle; + /** point shape 映射 */ + readonly shape?: string | ((x?: any, y?: any, color?: any) => string); + /** 大小映射,先简化处理为确定值 */ + readonly size?: number; + /** 样式映射 */ + readonly style?: ShapeStyle | ((x?: any, y?: any, color?: any) => ShapeStyle); }; } diff --git a/src/plots/pie/adaptor.ts b/src/plots/pie/adaptor.ts index 0ff2d63046..909409447b 100644 --- a/src/plots/pie/adaptor.ts +++ b/src/plots/pie/adaptor.ts @@ -1,8 +1,7 @@ import { deepMix, each, get, isFunction } from '@antv/util'; import { Params } from '../../core/adaptor'; -import { tooltip } from '../../common/adaptor'; +import { tooltip, interaction, animation, theme } from '../../common/adaptor'; import { flow } from '../../utils'; -import { Interaction } from '../../types/interaction'; import { StatisticContentStyle, StatisticTitleStyle } from './constants'; import { PieOptions } from './types'; import { getStatisticData } from './utils'; @@ -15,7 +14,6 @@ function field(params: Params): Params { const { chart, options } = params; const { data, angleField, colorField, color } = options; - // TODO 饼图数据非法处理 chart.data(data); const geometry = chart.interval().position(`1*${angleField}`).adjust({ type: 'stack' }); @@ -206,21 +204,6 @@ function annotation(params: Params): Params { return params; } -/** - * Interaction 配置 - * @param params - */ -export function interaction(params: Params): Params { - const { chart, options } = params; - const { interactions } = options; - - each(interactions, (i: Interaction) => { - chart.interaction(i.name, i.cfg || {}); - }); - - return params; -} - /** * 折线图适配器 * @param chart @@ -228,5 +211,5 @@ export function interaction(params: Params): Params { */ export function adaptor(params: Params) { // flow 的方式处理所有的配置到 G2 API - flow(field, meta, coord, legend, tooltip, label, style, annotation, interaction)(params); + flow(field, meta, theme, coord, legend, tooltip, label, style, annotation, interaction, animation)(params); } diff --git a/src/plots/tiny-line/adaptor.ts b/src/plots/tiny-line/adaptor.ts index f547a15d71..31c7d0475a 100644 --- a/src/plots/tiny-line/adaptor.ts +++ b/src/plots/tiny-line/adaptor.ts @@ -1,4 +1,3 @@ -import { Geometry } from '@antv/g2'; import { Params } from '../../core/adaptor'; import { flow } from '../../utils'; import { TinyLineOptions } from './types'; @@ -105,11 +104,26 @@ function shape(params: Params): Params { return params; } +/** + * 设置全局主题配置 + * @param params + */ +export function theme(params: Params): Params { + const { chart, options } = params; + const { theme } = options; + + // 存在主题才设置主题 + if (theme) { + chart.theme(theme); + } + return params; +} + /** * 迷你折线图适配器 * @param chart * @param options */ export function adaptor(params: Params) { - flow(field, meta, axis, legend, tooltip, style, shape)(params); + flow(field, meta, theme, axis, legend, tooltip, style, shape)(params); } diff --git a/src/plots/tiny-line/types.ts b/src/plots/tiny-line/types.ts index 1d72c16376..77c461fa1a 100644 --- a/src/plots/tiny-line/types.ts +++ b/src/plots/tiny-line/types.ts @@ -8,6 +8,8 @@ export interface TinyLineOptions extends ChartOptions { readonly data: number[]; /** 数据字段元信息 */ readonly meta?: Record; + /** 主题,G2 主题,字符串或者 theme object */ + readonly theme?: string | object; /** 是否平滑 */ readonly smooth?: boolean; /** 是否连接空数据 */ diff --git a/src/types/animation.ts b/src/types/animation.ts index 115dfd22e8..434e47cfc6 100644 --- a/src/types/animation.ts +++ b/src/types/animation.ts @@ -1 +1,3 @@ -export type Animation = {}; +import { AnimateOption } from '@antv/g2/lib/interface'; + +export type Animation = AnimateOption | false; diff --git a/src/types/common.ts b/src/types/common.ts index 5a9dfdcb02..4b32d04d9d 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -98,7 +98,7 @@ export type Options = ChartOptions & { /** 主题,G2 主题,字符串或者 theme object */ readonly theme?: string | object; /** 颜色色板 */ - readonly color?: string | string[]; + readonly color?: string | string[] | ((...args: any[]) => string); readonly xAxis?: Axis; readonly yAxis?: Axis; readonly label?: Label; From 62683d5c1788269b5ac9a19196309405294d9ff0 Mon Sep 17 00:00:00 2001 From: zqlu Date: Wed, 22 Jul 2020 11:45:14 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat(v2/column):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E6=9F=B1=E5=BD=A2=E5=9B=BE=20(#1316)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(v2/column): add column plot * feat(v2/column): export ColumnOptions & add findGeometry helper * feat(v2/column): add label formatter unit test * feat(v2/column): simplify findGeometry signature * feat(v2/column): changes with cr --- __tests__/data/sales.ts | 8 ++ __tests__/unit/plots/column/axis-spec.ts | 75 ++++++++++++ __tests__/unit/plots/column/index-spec.ts | 74 ++++++++++++ __tests__/unit/plots/column/label-spec.ts | 90 +++++++++++++++ __tests__/unit/plots/column/style-spec.ts | 61 ++++++++++ src/common/helper.ts | 10 ++ src/constant.ts | 13 +++ src/core/plot.ts | 4 +- src/index.ts | 3 + src/plots/column/adaptor.ts | 133 ++++++++++++++++++++++ src/plots/column/index.ts | 21 ++++ src/plots/column/types.ts | 13 +++ src/plots/line/adaptor.ts | 9 +- 13 files changed, 507 insertions(+), 7 deletions(-) create mode 100644 __tests__/data/sales.ts create mode 100644 __tests__/unit/plots/column/axis-spec.ts create mode 100644 __tests__/unit/plots/column/index-spec.ts create mode 100644 __tests__/unit/plots/column/label-spec.ts create mode 100644 __tests__/unit/plots/column/style-spec.ts create mode 100644 src/common/helper.ts create mode 100644 src/constant.ts create mode 100644 src/plots/column/adaptor.ts create mode 100644 src/plots/column/index.ts create mode 100644 src/plots/column/types.ts diff --git a/__tests__/data/sales.ts b/__tests__/data/sales.ts new file mode 100644 index 0000000000..faea682969 --- /dev/null +++ b/__tests__/data/sales.ts @@ -0,0 +1,8 @@ +export const salesByArea = [ + { area: '东北', sales: 2681567.469000001 }, + { area: '中南', sales: 4137415.0929999948 }, + { area: '华东', sales: 4684506.442 }, + { area: '华北', sales: 2447301.017000004 }, + { area: '西北', sales: 815039.5959999998 }, + { area: '西南', sales: 1303124.508000002 }, +]; diff --git a/__tests__/unit/plots/column/axis-spec.ts b/__tests__/unit/plots/column/axis-spec.ts new file mode 100644 index 0000000000..f87a1091d3 --- /dev/null +++ b/__tests__/unit/plots/column/axis-spec.ts @@ -0,0 +1,75 @@ +import { Column } from '../../../../src'; +import { salesByArea } from '../../../data/sales'; +import { createDiv } from '../../../utils/dom'; + +describe('column axis', () => { + it('meta', () => { + const formatter = (v) => `${Math.floor(v / 10000)}万`; + const column = new Column(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'area', + yField: 'sales', + meta: { + sales: { + nice: true, + formatter, + }, + }, + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + // @ts-ignore + expect(geometry.scales.sales.nice).toBe(true); + expect(geometry.scales.sales.formatter).toBe(formatter); + }); + + it('xAxis', () => { + const column = new Column(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'area', + yField: 'sales', + xAxis: { + label: { + rotate: -Math.PI / 2, + }, + }, + }); + + column.render(); + const axisOptions = column.chart.getOptions().axes; + + // @ts-ignore + expect(axisOptions.area.label.rotate).toBe(-Math.PI / 2); + }); + + it('yAxis', () => { + const column = new Column(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'area', + yField: 'sales', + yAxis: { + minLimit: 10000, + nice: true, + }, + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + const axisOptions = column.chart.getOptions().axes; + + // @ts-ignore + expect(axisOptions.sales.minLimit).toBe(10000); + expect(geometry.scales.sales.minLimit).toBe(10000); + // @ts-ignore + expect(geometry.scales.sales.nice).toBe(true); + }); +}); diff --git a/__tests__/unit/plots/column/index-spec.ts b/__tests__/unit/plots/column/index-spec.ts new file mode 100644 index 0000000000..ddbab09235 --- /dev/null +++ b/__tests__/unit/plots/column/index-spec.ts @@ -0,0 +1,74 @@ +import { Column } from '../../../../src'; +import { salesByArea } from '../../../data/sales'; +import { createDiv } from '../../../utils/dom'; + +describe('column', () => { + it('x*y', () => { + const column = new Column(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'area', + yField: 'sales', + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + const positionFields = geometry.getAttribute('position').getFields(); + + // 类型 + expect(geometry.type).toBe('interval'); + // 图形元素个数 + expect(column.chart.geometries[0].elements.length).toBe(salesByArea.length); + // x & y + expect(positionFields).toHaveLength(2); + expect(positionFields[0]).toBe('area'); + expect(positionFields[1]).toBe('sales'); + }); + + it('x*y*color', () => { + const column = new Column(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'area', + yField: 'sales', + colorField: 'area', + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + const colorFields = geometry.getAttribute('color').getFields(); + + expect(colorFields).toHaveLength(1); + expect(colorFields[0]).toBe('area'); + }); + + it('x*y*color with color', () => { + const palette = ['red', 'yellow', 'green']; + const column = new Column(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'area', + yField: 'sales', + colorField: 'area', + color: palette, + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + const colorAttribute = geometry.getAttribute('color'); + const colorFields = colorAttribute.getFields(); + + expect(colorFields).toHaveLength(1); + expect(colorFields[0]).toBe('area'); + geometry.elements.forEach((element, index) => { + const color = element.getModel().color; + expect(color).toBe(palette[index % palette.length]); + }); + }); +}); diff --git a/__tests__/unit/plots/column/label-spec.ts b/__tests__/unit/plots/column/label-spec.ts new file mode 100644 index 0000000000..3678410d91 --- /dev/null +++ b/__tests__/unit/plots/column/label-spec.ts @@ -0,0 +1,90 @@ +import { Column } from '../../../../src'; +import { salesByArea } from '../../../data/sales'; +import { createDiv } from '../../../utils/dom'; + +describe('column label', () => { + it('position: top', () => { + const column = new Column(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'area', + yField: 'sales', + 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(salesByArea.length); + labelGroups.forEach((label, index) => { + expect(label.get('children')[0].attr('text')).toBe(`${Math.floor(salesByArea[index].sales / 10000)}万`); + }); + }); + + it('label position middle', () => { + const column = new Column(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'area', + yField: 'sales', + meta: { + sales: { + nice: true, + formatter: (v) => `${Math.floor(v / 10000)}万`, + }, + }, + label: { + position: 'middle', + }, + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + + // @ts-ignore + expect(geometry.labelOption.cfg).toEqual({ position: 'middle' }); + }); + + it('label position bottom', () => { + const column = new Column(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'area', + yField: 'sales', + meta: { + sales: { + nice: true, + formatter: (v) => `${Math.floor(v / 10000)}万`, + }, + }, + label: { + position: 'bottom', + }, + }); + + column.render(); + + const geometry = column.chart.geometries[0]; + + // @ts-ignore + expect(geometry.labelOption.cfg).toEqual({ position: 'bottom' }); + }); +}); diff --git a/__tests__/unit/plots/column/style-spec.ts b/__tests__/unit/plots/column/style-spec.ts new file mode 100644 index 0000000000..7c4320bded --- /dev/null +++ b/__tests__/unit/plots/column/style-spec.ts @@ -0,0 +1,61 @@ +import { Column } from '../../../../src'; +import { salesByArea } from '../../../data/sales'; +import { createDiv } from '../../../utils/dom'; + +describe('column style', () => { + it('style config', () => { + const column = new Column(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'area', + yField: 'sales', + meta: { + sales: { + nice: true, + formatter: (v) => `${Math.floor(v / 10000)}万`, + }, + }, + columnStyle: { + stroke: 'black', + lineWidth: 2, + }, + }); + + 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); + }); + + it('style callback', () => { + const column = new Column(createDiv(), { + width: 400, + height: 300, + data: salesByArea, + xField: 'area', + yField: 'sales', + meta: { + sales: { + nice: true, + formatter: (v) => `${Math.floor(v / 10000)}万`, + }, + }, + columnStyle: (x, y) => { + return { + stroke: 'black', + lineWidth: 2, + }; + }, + }); + + 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); + }); +}); diff --git a/src/common/helper.ts b/src/common/helper.ts new file mode 100644 index 0000000000..d3aef49b18 --- /dev/null +++ b/src/common/helper.ts @@ -0,0 +1,10 @@ +import { Chart, Geometry } from '@antv/g2'; + +/** + * 在 Chart 中查找第一个指定 type 类型的 geometry + * @param chart + * @param type + */ +export function findGeometry(chart: Chart, type: string): Geometry { + return chart.geometries.find((g: Geometry) => g.type === type); +} diff --git a/src/constant.ts b/src/constant.ts new file mode 100644 index 0000000000..0c523b935e --- /dev/null +++ b/src/constant.ts @@ -0,0 +1,13 @@ +/** + * 需要从轴配置中提取出来作为 meta 的属性 key 列表 + */ +export const AXIS_META_CONFIG_KEYS = [ + 'tickCount', + 'tickInterval', + 'min', + 'max', + 'nice', + 'minLimit', + 'maxLimit', + 'tickMethod', +]; diff --git a/src/core/plot.ts b/src/core/plot.ts index b1042a736d..ced92d2271 100644 --- a/src/core/plot.ts +++ b/src/core/plot.ts @@ -8,11 +8,11 @@ import { ChartOptions, Data } from '../types'; */ export abstract class Plot { /** plot 类型名称 */ - public abstract type: string = 'base'; + public abstract readonly type: string = 'base'; /** plot 的 schema 配置 */ public options: O; /** plot 绘制的 dom */ - public container: HTMLElement; + public readonly container: HTMLElement; /** G2 chart 实例 */ public chart: Chart; /** resizer unbind */ diff --git a/src/index.ts b/src/index.ts index fb583a0152..fc620ac609 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,9 @@ export * from './types'; // 折线图及类型定义 export { Line, LineOptions } from './plots/line'; +// 柱形图及类型定义 +export { Column, ColumnOptions } from './plots/column'; + // 饼图及类型定义 export { Pie, PieOptions } from './plots/pie'; diff --git a/src/plots/column/adaptor.ts b/src/plots/column/adaptor.ts new file mode 100644 index 0000000000..fe2259751a --- /dev/null +++ b/src/plots/column/adaptor.ts @@ -0,0 +1,133 @@ +import { Geometry, Chart } from '@antv/g2'; +import { deepMix, isFunction } from '@antv/util'; +import { Params } from '../../core/adaptor'; +import { findGeometry } from '../../common/helper'; +import { flow, pick } from '../../utils'; +import { ColumnOptions } from './types'; +import { AXIS_META_CONFIG_KEYS } from '../../constant'; + +/** + * 字段 + * @param params + */ +function field(params: Params): Params { + const { chart, options } = params; + const { data, xField, yField, colorField, color } = options; + + chart.data(data); + const geometry = chart.interval().position(`${xField}*${yField}`); + + if (colorField) { + geometry.color(colorField, color); + } + + return params; +} + +/** + * meta 配置 + * @param params + */ +function meta(params: Params): Params { + const { chart, options } = params; + const { meta, xAxis, yAxis, xField, yField } = options; + + const scales = deepMix({}, meta, { + [xField]: pick(xAxis, AXIS_META_CONFIG_KEYS), + [yField]: 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, yField } = options; + + // 为 false 则是不显示轴 + if (xAxis === false) { + chart.axis(xField, false); + } else { + chart.axis(xField, xAxis); + } + + if (yAxis === false) { + chart.axis(yField, false); + } else { + chart.axis(yField, 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, colorField, columnStyle } = options; + + const geometry = findGeometry(chart, 'interval'); + if (columnStyle && geometry) { + if (isFunction(columnStyle)) { + geometry.style(`${xField}*${yField}*${colorField}`, columnStyle); + } else { + geometry.style(columnStyle); + } + } + return params; +} + +/** + * 数据标签 + * @param params + */ +function label(params: Params): Params { + const { chart, options } = params; + const { label, yField } = options; + + const geometry = findGeometry(chart, 'interval'); + + if (!label) { + geometry.label(false); + } else { + const { callback, ...cfg } = label; + geometry.label({ + fields: [yField], + callback, + cfg, + }); + } + + return params; +} + +/** + * 柱形图适配器 + * @param params + */ +export function adaptor(params: Params) { + return flow(field, meta, axis, legend, style, label)(params); +} diff --git a/src/plots/column/index.ts b/src/plots/column/index.ts new file mode 100644 index 0000000000..04f901f324 --- /dev/null +++ b/src/plots/column/index.ts @@ -0,0 +1,21 @@ +import { Plot } from '../../core/plot'; +import { ColumnOptions } from './types'; +import { adaptor } from './adaptor'; +import { Adaptor } from '../../core/adaptor'; + +export { ColumnOptions }; + +/** + * 柱形图 + */ +export class Column extends Plot { + /** 图表类型 */ + public readonly type: string = 'column'; + + /** + * 获取 柱形图 的适配器 + */ + protected getSchemaAdaptor(): Adaptor { + return adaptor; + } +} diff --git a/src/plots/column/types.ts b/src/plots/column/types.ts new file mode 100644 index 0000000000..08f5a39c5c --- /dev/null +++ b/src/plots/column/types.ts @@ -0,0 +1,13 @@ +import { Options } from '../../types'; +import { ShapeStyle } from '../../types/style'; + +export interface ColumnOptions extends Options { + /** x 轴字段 */ + readonly xField?: string; + /** y 轴字段 */ + readonly yField?: string; + /** 颜色字段,可选 */ + readonly colorField?: string; + /** 柱子样式配置,可选 */ + readonly columnStyle?: ShapeStyle | ((x: any, y: any, color?: any) => ShapeStyle); +} diff --git a/src/plots/line/adaptor.ts b/src/plots/line/adaptor.ts index f407f2eb21..b01bcf4140 100644 --- a/src/plots/line/adaptor.ts +++ b/src/plots/line/adaptor.ts @@ -4,6 +4,7 @@ import { Params } from '../../core/adaptor'; import { tooltip, interaction, animation, theme } from '../../common/adaptor'; import { flow, pick } from '../../utils'; import { LineOptions } from './types'; +import { AXIS_META_CONFIG_KEYS } from '../../constant'; /** * 字段 @@ -31,12 +32,10 @@ function meta(params: Params): Params { const { chart, options } = params; const { meta, xAxis, yAxis, xField, yField } = options; - const KEYS = ['tickCount', 'tickInterval', 'min', 'max', 'nice', 'minLimit', 'maxLimit', 'tickMethod']; - // meta 直接是 scale 的信息 const scales = deepMix({}, meta, { - [xField]: pick(xAxis, KEYS), - [yField]: pick(yAxis, KEYS), + [xField]: pick(xAxis, AXIS_META_CONFIG_KEYS), + [yField]: pick(yAxis, AXIS_META_CONFIG_KEYS), }); chart.scale(scales); @@ -60,7 +59,7 @@ function axis(params: Params): Params { } if (yAxis === false) { - chart.axis(xField, false); + chart.axis(yField, false); } else { chart.axis(yField, yAxis); } From d8273122ade6f506e38e960be7219cf58f8af4e6 Mon Sep 17 00:00:00 2001 From: Kasmine <736929286@qq.com> Date: Wed, 22 Jul 2020 14:25:17 +0800 Subject: [PATCH 3/5] v2 pie/enhance (#1318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(v2/pie): 饼图style回调修改 * feat(pie-legend-interaction): 饼图图例active交互 * feat(v2-pie-interaction): 饼图图例交互 饼图图例激活的时候,默认带上legend-active交互,避免外部移除影响此交互行为的失效,end 交互同legend-active Co-authored-by: xinming --- __tests__/unit/plots/pie/index-spec.ts | 4 +- __tests__/unit/plots/pie/interaction-spec.ts | 41 +++++++++++++++- src/plots/pie/adaptor.ts | 3 +- src/plots/pie/interaction/index.ts | 15 ++++++ .../pie/interaction/pie-legend-action.ts | 47 +++++++++++++++++++ .../pie-statistic-action.ts} | 10 +--- src/utils/g-util.ts | 8 ++++ 7 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 src/plots/pie/interaction/index.ts create mode 100644 src/plots/pie/interaction/pie-legend-action.ts rename src/plots/pie/{interaction.ts => interaction/pie-statistic-action.ts} (86%) create mode 100644 src/utils/g-util.ts diff --git a/__tests__/unit/plots/pie/index-spec.ts b/__tests__/unit/plots/pie/index-spec.ts index ab5bc3b4c5..e58711e18d 100644 --- a/__tests__/unit/plots/pie/index-spec.ts +++ b/__tests__/unit/plots/pie/index-spec.ts @@ -120,8 +120,8 @@ describe('pie', () => { color: ['blue', 'red', 'yellow', 'lightgreen', 'lightblue', 'pink'], radius: 0.8, innerRadius: 0.5, - pieStyle: (item) => ({ - fill: item === 'item1' ? 'blue' : 'red', + pieStyle: (value, type) => ({ + fill: type === 'item1' ? 'blue' : 'red', lineWidth: 3, stroke: 'yellow', }), diff --git a/__tests__/unit/plots/pie/interaction-spec.ts b/__tests__/unit/plots/pie/interaction-spec.ts index 6c741e2ba5..0f491dd05e 100644 --- a/__tests__/unit/plots/pie/interaction-spec.ts +++ b/__tests__/unit/plots/pie/interaction-spec.ts @@ -3,7 +3,8 @@ import InteractionContext from '@antv/g2/lib/interaction/context'; import { delay } from '../../../utils/delay'; import { createDiv } from '../../../utils/dom'; import { Pie } from '../../../../src'; -import { StatisticAction } from '../../../../src/plots/pie/interaction'; +import { StatisticAction } from '../../../../src/plots/pie/interaction/pie-statistic-action'; +import { PieLegendAction } from '../../../../src/plots/pie/interaction/pie-legend-action'; describe('register interaction', () => { it('创建 "pie-statistic" action', () => { @@ -56,3 +57,41 @@ describe('register interaction', () => { expect(annotations[0].extra.content).toBe('Total'); }); }); + +describe('G2 内置interactions', () => { + const pie = new Pie(createDiv(), { + width: 400, + height: 300, + data: [ + { type: 'item1', value: 10 }, + { type: 'item2', value: 13 }, + ], + angleField: 'value', + colorField: 'type', + radius: 0.8, + innerRadius: 0.64, + statistic: { + title: { formatter: (item, data) => (!Array.isArray(data) ? item.title : 'Total') }, + }, + }); + + pie.render(); + it('交互: element-single-selected', () => { + pie.update({ + ...pie.options, + interactions: [{ name: 'element-single-selected' }], + }); + + expect(pie.chart.interactions['element-single-selected']).toBeDefined(); + }); + + it('交互: pie-legend-active', () => { + pie.update({ + ...pie.options, + interactions: [{ name: 'pie-legend-active' }], + }); + + expect(pie.chart.interactions['pie-legend-active']).toBeDefined(); + expect(pie.chart.interactions['legend-active']).toBeDefined(); + }); +}); diff --git a/src/plots/pie/adaptor.ts b/src/plots/pie/adaptor.ts index 909409447b..f8a3a149c8 100644 --- a/src/plots/pie/adaptor.ts +++ b/src/plots/pie/adaptor.ts @@ -109,8 +109,7 @@ function style(params: Params): Params { const geometry = chart.geometries[0]; if (pieStyle && geometry) { if (isFunction(pieStyle)) { - // 为了兼容,colorField 放第一位 - geometry.style(colorField ? `${colorField}*${angleField}` : angleField, pieStyle); + geometry.style(`${angleField}*${colorField}`, pieStyle); } else { geometry.style(pieStyle); } diff --git a/src/plots/pie/interaction/index.ts b/src/plots/pie/interaction/index.ts new file mode 100644 index 0000000000..73ec2a6e45 --- /dev/null +++ b/src/plots/pie/interaction/index.ts @@ -0,0 +1,15 @@ +import { registerAction, registerInteraction } from '@antv/g2'; +import { PieLegendAction } from './pie-legend-action'; +import { StatisticAction } from './pie-statistic-action'; + +registerAction('pie-statistic', StatisticAction); +registerInteraction('pie-statistic-active', { + start: [{ trigger: 'element:mouseenter', action: 'pie-statistic:change' }], + end: [{ trigger: 'element:mouseleave', action: 'pie-statistic:reset' }], +}); + +registerAction('pie-legend', PieLegendAction); +registerInteraction('pie-legend-active', { + start: [{ trigger: 'legend-item:mouseenter', action: 'pie-legend:active' }], + end: [{ trigger: 'legend-item:mouseleave', action: [] }], +}); diff --git a/src/plots/pie/interaction/pie-legend-action.ts b/src/plots/pie/interaction/pie-legend-action.ts new file mode 100644 index 0000000000..cee182019c --- /dev/null +++ b/src/plots/pie/interaction/pie-legend-action.ts @@ -0,0 +1,47 @@ +import { Util } from '@antv/g2'; +import Element from '@antv/g2/lib/geometry/element'; +import { Action } from '@antv/g2/lib/interaction'; +import { getDelegationObject } from '@antv/g2/lib/interaction/action/util'; +import { groupTransform } from '../../../utils/g-util'; + +/** + * 饼图 图例激活 action + */ +export class PieLegendAction extends Action { + init() { + const { view } = this.context; + view.interaction('legend-active'); + } + /** + * 获取激活的图形元素 + */ + private getActiveElements(): Element[] { + const delegateObject = getDelegationObject(this.context); + if (delegateObject) { + const view = this.context.view; + const { component, item } = delegateObject; + const field = component.get('field'); + if (field) { + const elements = view.geometries[0].elements; + return elements.filter((ele) => ele.getModel().data[field] === item.value); + } + } + return []; + } + + public active() { + const elements = this.getActiveElements(); + elements.forEach((element) => { + const coordinate = element.geometry.coordinate; + if (coordinate.isPolar && coordinate.isTransposed) { + const { startAngle, endAngle } = Util.getAngle(element.getModel(), coordinate); + const middleAngle = (startAngle + endAngle) / 2; + /** offset 偏移 */ + const r = 7.5; + const x = r * Math.cos(middleAngle); + const y = r * Math.sin(middleAngle); + groupTransform(element.shape, [['t', x, y]]); + } + }); + } +} diff --git a/src/plots/pie/interaction.ts b/src/plots/pie/interaction/pie-statistic-action.ts similarity index 86% rename from src/plots/pie/interaction.ts rename to src/plots/pie/interaction/pie-statistic-action.ts index 714f851d39..e7d669d912 100644 --- a/src/plots/pie/interaction.ts +++ b/src/plots/pie/interaction/pie-statistic-action.ts @@ -1,8 +1,7 @@ -import { registerAction, registerInteraction } from '@antv/g2'; import { Action } from '@antv/g2/lib/interaction'; import { ComponentOption } from '@antv/g2/lib/interface'; import { each, get } from '@antv/util'; -import { getStatisticData } from './utils'; +import { getStatisticData } from '../utils'; /** * Pie 中心文本事件的 Action @@ -75,10 +74,3 @@ export class StatisticAction extends Action { view.render(true); } } - -registerAction('pie-statistic', StatisticAction); - -registerInteraction('pie-statistic-active', { - start: [{ trigger: 'element:mouseenter', action: 'pie-statistic:change' }], - end: [{ trigger: 'element:mouseleave', action: 'pie-statistic:reset' }], -}); diff --git a/src/utils/g-util.ts b/src/utils/g-util.ts new file mode 100644 index 0000000000..806301e76f --- /dev/null +++ b/src/utils/g-util.ts @@ -0,0 +1,8 @@ +import { Util } from '@antv/g2'; +import { IGroup, IShape } from '@antv/g2/lib/dependents'; + +export function groupTransform(group: IGroup | IShape, actions) { + const ulMatrix = [1, 0, 0, 0, 1, 0, 0, 0, 1]; + const matrix = Util.transform(ulMatrix, actions); + group.setMatrix(matrix); +} From 83b67f4f4ed4a9af4a275fbb6a5e11629890c24b Mon Sep 17 00:00:00 2001 From: connono <36756846+connono@users.noreply.github.com> Date: Wed, 22 Jul 2020 15:45:22 +0800 Subject: [PATCH 4/5] feat: add progress (#1317) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add progress * fix: 修改progress type * fix: 修改progress type * fix: 修改单测shapestyle为function * fix: innerRadius & radius 增加默认值 * fix: 修改默认值 Co-authored-by: yinshi.wyl --- __tests__/unit/plots/progress/index-spec.ts | 163 ++++++++++++++ .../unit/plots/ring-progress/index-spec.ts | 203 ++++++++++++++++++ src/index.ts | 6 + src/plots/progress/adaptor.ts | 122 +++++++++++ src/plots/progress/index.ts | 18 ++ src/plots/progress/types.ts | 15 ++ src/plots/ring-progress/adaptor.ts | 126 +++++++++++ src/plots/ring-progress/index.ts | 18 ++ src/plots/ring-progress/types.ts | 19 ++ 9 files changed, 690 insertions(+) create mode 100644 __tests__/unit/plots/progress/index-spec.ts create mode 100644 __tests__/unit/plots/ring-progress/index-spec.ts create mode 100644 src/plots/progress/adaptor.ts create mode 100644 src/plots/progress/index.ts create mode 100644 src/plots/progress/types.ts create mode 100644 src/plots/ring-progress/adaptor.ts create mode 100644 src/plots/ring-progress/index.ts create mode 100644 src/plots/ring-progress/types.ts diff --git a/__tests__/unit/plots/progress/index-spec.ts b/__tests__/unit/plots/progress/index-spec.ts new file mode 100644 index 0000000000..89d847a97d --- /dev/null +++ b/__tests__/unit/plots/progress/index-spec.ts @@ -0,0 +1,163 @@ +import { Progress } from '../../../../src'; +import { createDiv } from '../../../utils/dom'; + +describe('progress', () => { + it('data', () => { + const progress = new Progress(createDiv(), { + width: 200, + height: 100, + percent: 0.6, + autoFit: false, + }); + + progress.render(); + expect(progress.chart.geometries[0].elements[0].getData().type).toBe('current'); + expect(progress.chart.geometries[0].elements[0].getData().percent).toBe(0.6); + expect(progress.chart.geometries[0].elements[0].shape.attr('fill')).toBe('#FAAD14'); + expect(progress.chart.geometries[0].elements[1].getData().type).toBe('target'); + expect(progress.chart.geometries[0].elements[1].getData().percent).toBe(0.4); + expect(progress.chart.geometries[0].elements[1].shape.attr('fill')).toBe('#E8EDF3'); + + progress.update({ + width: 200, + height: 100, + percent: 0.5, + autoFit: false, + }); + expect(progress.chart.geometries[0].elements[0].getData().type).toBe('current'); + expect(progress.chart.geometries[0].elements[0].getData().percent).toBe(0.5); + expect(progress.chart.geometries[0].elements[0].shape.attr('fill')).toBe('#FAAD14'); + expect(progress.chart.geometries[0].elements[1].getData().type).toBe('target'); + expect(progress.chart.geometries[0].elements[1].getData().percent).toBe(0.5); + expect(progress.chart.geometries[0].elements[1].shape.attr('fill')).toBe('#E8EDF3'); + }); + + it('data with color', () => { + const progress = new Progress(createDiv(), { + width: 200, + height: 100, + percent: 0.6, + color: ['#123456', '#654321'], + autoFit: false, + }); + + progress.render(); + expect(progress.chart.geometries[0].elements[0].getData().type).toBe('current'); + expect(progress.chart.geometries[0].elements[0].getData().percent).toBe(0.6); + expect(progress.chart.geometries[0].elements[0].shape.attr('fill')).toBe('#123456'); + expect(progress.chart.geometries[0].elements[1].getData().type).toBe('target'); + expect(progress.chart.geometries[0].elements[1].getData().percent).toBe(0.4); + expect(progress.chart.geometries[0].elements[1].shape.attr('fill')).toBe('#654321'); + + progress.update({ + width: 200, + height: 100, + percent: 0.6, + color: () => ['#654321', '#123456'], + autoFit: false, + }); + expect(progress.chart.geometries[0].elements[0].getData().type).toBe('current'); + expect(progress.chart.geometries[0].elements[0].getData().percent).toBe(0.6); + expect(progress.chart.geometries[0].elements[0].shape.attr('fill')).toBe('#654321'); + expect(progress.chart.geometries[0].elements[1].getData().type).toBe('target'); + expect(progress.chart.geometries[0].elements[1].getData().percent).toBe(0.4); + expect(progress.chart.geometries[0].elements[1].shape.attr('fill')).toBe('#123456'); + }); + + it('data with progressStyle', () => { + const progress = new Progress(createDiv(), { + width: 200, + height: 100, + percent: 0.6, + progressStyle: { + stroke: '#123456', + lineWidth: 2, + lineDash: [2, 2], + }, + autoFit: false, + }); + + progress.render(); + expect(progress.chart.geometries[0].elements[0].getData().type).toBe('current'); + expect(progress.chart.geometries[0].elements[0].getData().percent).toBe(0.6); + expect(progress.chart.geometries[0].elements[0].shape.attr('fill')).toBe('#FAAD14'); + expect(progress.chart.geometries[0].elements[0].shape.attr('stroke')).toBe('#123456'); + expect(progress.chart.geometries[0].elements[0].shape.attr('lineWidth')).toBe(2); + expect(progress.chart.geometries[0].elements[0].shape.attr('lineDash')).toEqual([2, 2]); + expect(progress.chart.geometries[0].elements[1].getData().type).toBe('target'); + expect(progress.chart.geometries[0].elements[1].getData().percent).toBe(0.4); + expect(progress.chart.geometries[0].elements[1].shape.attr('fill')).toBe('#E8EDF3'); + expect(progress.chart.geometries[0].elements[1].shape.attr('stroke')).toBe('#123456'); + expect(progress.chart.geometries[0].elements[1].shape.attr('lineWidth')).toBe(2); + expect(progress.chart.geometries[0].elements[1].shape.attr('lineDash')).toEqual([2, 2]); + + const progressStyle = (x, percent, type) => { + if (type === 'current') { + return percent > 0.5 + ? { + stroke: '#654321', + lineWidth: 4, + lineDash: [4, 4], + } + : { + stroke: '#123456', + lineWidth: 4, + lineDash: [4, 4], + }; + } else if (type === 'target') { + return percent >= 0.5 + ? { + stroke: '#654321', + lineWidth: 4, + lineDash: [4, 4], + } + : { + stroke: '#123456', + lineWidth: 4, + lineDash: [4, 4], + }; + } + }; + + progress.update({ + width: 200, + height: 100, + percent: 0.6, + progressStyle, + autoFit: false, + }); + expect(progress.chart.geometries[0].elements[0].getData().type).toBe('current'); + expect(progress.chart.geometries[0].elements[0].getData().percent).toBe(0.6); + expect(progress.chart.geometries[0].elements[0].shape.attr('fill')).toBe('#FAAD14'); + expect(progress.chart.geometries[0].elements[0].shape.attr('stroke')).toBe('#654321'); + expect(progress.chart.geometries[0].elements[0].shape.attr('lineWidth')).toBe(4); + expect(progress.chart.geometries[0].elements[0].shape.attr('lineDash')).toEqual([4, 4]); + expect(progress.chart.geometries[0].elements[1].getData().type).toBe('target'); + expect(progress.chart.geometries[0].elements[1].getData().percent).toBe(0.4); + expect(progress.chart.geometries[0].elements[1].shape.attr('fill')).toBe('#E8EDF3'); + expect(progress.chart.geometries[0].elements[1].shape.attr('stroke')).toBe('#123456'); + expect(progress.chart.geometries[0].elements[1].shape.attr('lineWidth')).toBe(4); + expect(progress.chart.geometries[0].elements[1].shape.attr('lineDash')).toEqual([4, 4]); + + progress.update({ + width: 200, + height: 100, + percent: 0.4, + progressStyle, + autoFit: false, + }); + + expect(progress.chart.geometries[0].elements[0].getData().type).toBe('current'); + expect(progress.chart.geometries[0].elements[0].getData().percent).toBe(0.4); + expect(progress.chart.geometries[0].elements[0].shape.attr('fill')).toBe('#FAAD14'); + expect(progress.chart.geometries[0].elements[0].shape.attr('stroke')).toBe('#123456'); + expect(progress.chart.geometries[0].elements[0].shape.attr('lineWidth')).toBe(4); + expect(progress.chart.geometries[0].elements[0].shape.attr('lineDash')).toEqual([4, 4]); + expect(progress.chart.geometries[0].elements[1].getData().type).toBe('target'); + expect(progress.chart.geometries[0].elements[1].getData().percent).toBe(0.6); + expect(progress.chart.geometries[0].elements[1].shape.attr('fill')).toBe('#E8EDF3'); + expect(progress.chart.geometries[0].elements[1].shape.attr('stroke')).toBe('#654321'); + expect(progress.chart.geometries[0].elements[1].shape.attr('lineWidth')).toBe(4); + expect(progress.chart.geometries[0].elements[1].shape.attr('lineDash')).toEqual([4, 4]); + }); +}); diff --git a/__tests__/unit/plots/ring-progress/index-spec.ts b/__tests__/unit/plots/ring-progress/index-spec.ts new file mode 100644 index 0000000000..a854546f32 --- /dev/null +++ b/__tests__/unit/plots/ring-progress/index-spec.ts @@ -0,0 +1,203 @@ +import { RingProgress } from '../../../../src'; +import { createDiv } from '../../../utils/dom'; + +describe('ring-progress', () => { + it('data', () => { + const ringGrogress = new RingProgress(createDiv(), { + radius: 1, + innerRadius: 0.5, + width: 200, + height: 100, + percent: 0.6, + autoFit: false, + }); + + ringGrogress.render(); + expect(ringGrogress.chart.geometries[0].coordinate.type).toBe('theta'); + expect(ringGrogress.chart.geometries[0].coordinate.radius).toBe(1); + expect(ringGrogress.chart.geometries[0].coordinate.innerRadius).toBe(0.5); + expect(ringGrogress.chart.geometries[0].elements[0].getData().type).toBe('current'); + expect(ringGrogress.chart.geometries[0].elements[0].getData().percent).toBe(0.6); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('fill')).toBe('#FAAD14'); + expect(ringGrogress.chart.geometries[0].elements[1].getData().type).toBe('target'); + expect(ringGrogress.chart.geometries[0].elements[1].getData().percent).toBe(0.4); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('fill')).toBe('#E8EDF3'); + + ringGrogress.update({ + radius: 1, + innerRadius: 0.5, + width: 200, + height: 100, + percent: 0.5, + autoFit: false, + }); + expect(ringGrogress.chart.geometries[0].coordinate.type).toBe('theta'); + expect(ringGrogress.chart.geometries[0].coordinate.radius).toBe(1); + expect(ringGrogress.chart.geometries[0].coordinate.innerRadius).toBe(0.5); + expect(ringGrogress.chart.geometries[0].elements[0].getData().type).toBe('current'); + expect(ringGrogress.chart.geometries[0].elements[0].getData().percent).toBe(0.5); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('fill')).toBe('#FAAD14'); + expect(ringGrogress.chart.geometries[0].elements[1].getData().type).toBe('target'); + expect(ringGrogress.chart.geometries[0].elements[1].getData().percent).toBe(0.5); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('fill')).toBe('#E8EDF3'); + }); + + it('data with color', () => { + const ringGrogress = new RingProgress(createDiv(), { + radius: 1, + innerRadius: 0.5, + width: 200, + height: 100, + percent: 0.6, + color: ['#123456', '#654321'], + autoFit: false, + }); + + ringGrogress.render(); + expect(ringGrogress.chart.geometries[0].elements[0].getData().type).toBe('current'); + expect(ringGrogress.chart.geometries[0].elements[0].getData().percent).toBe(0.6); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('fill')).toBe('#123456'); + expect(ringGrogress.chart.geometries[0].elements[1].getData().type).toBe('target'); + expect(ringGrogress.chart.geometries[0].elements[1].getData().percent).toBe(0.4); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('fill')).toBe('#654321'); + + ringGrogress.update({ + radius: 1, + innerRadius: 0.5, + width: 200, + height: 100, + percent: 0.6, + color: () => ['#654321', '#123456'], + autoFit: false, + }); + expect(ringGrogress.chart.geometries[0].elements[0].getData().type).toBe('current'); + expect(ringGrogress.chart.geometries[0].elements[0].getData().percent).toBe(0.6); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('fill')).toBe('#654321'); + expect(ringGrogress.chart.geometries[0].elements[1].getData().type).toBe('target'); + expect(ringGrogress.chart.geometries[0].elements[1].getData().percent).toBe(0.4); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('fill')).toBe('#123456'); + }); + + it('data with progressStyle', () => { + const ringGrogress = new RingProgress(createDiv(), { + radius: 1, + innerRadius: 0.5, + width: 200, + height: 100, + percent: 0.6, + progressStyle: { + stroke: '#123456', + lineWidth: 2, + lineDash: [2, 2], + }, + autoFit: false, + }); + + ringGrogress.render(); + expect(ringGrogress.chart.geometries[0].elements[0].getData().type).toBe('current'); + expect(ringGrogress.chart.geometries[0].elements[0].getData().percent).toBe(0.6); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('fill')).toBe('#FAAD14'); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('stroke')).toBe('#123456'); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('lineWidth')).toBe(2); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('lineDash')).toEqual([2, 2]); + expect(ringGrogress.chart.geometries[0].elements[1].getData().type).toBe('target'); + expect(ringGrogress.chart.geometries[0].elements[1].getData().percent).toBe(0.4); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('fill')).toBe('#E8EDF3'); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('stroke')).toBe('#123456'); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('lineWidth')).toBe(2); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('lineDash')).toEqual([2, 2]); + + const progressStyle = (x, percent, type) => { + if (type === 'current') { + return percent > 0.5 + ? { + stroke: '#654321', + lineWidth: 4, + lineDash: [4, 4], + } + : { + stroke: '#123456', + lineWidth: 4, + lineDash: [4, 4], + }; + } else if (type === 'target') { + return percent >= 0.5 + ? { + stroke: '#654321', + lineWidth: 4, + lineDash: [4, 4], + } + : { + stroke: '#123456', + lineWidth: 4, + lineDash: [4, 4], + }; + } + }; + + ringGrogress.update({ + radius: 1, + innerRadius: 0.5, + width: 200, + height: 100, + percent: 0.6, + progressStyle, + autoFit: false, + }); + expect(ringGrogress.chart.geometries[0].elements[0].getData().type).toBe('current'); + expect(ringGrogress.chart.geometries[0].elements[0].getData().percent).toBe(0.6); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('fill')).toBe('#FAAD14'); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('stroke')).toBe('#654321'); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('lineWidth')).toBe(4); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('lineDash')).toEqual([4, 4]); + expect(ringGrogress.chart.geometries[0].elements[1].getData().type).toBe('target'); + expect(ringGrogress.chart.geometries[0].elements[1].getData().percent).toBe(0.4); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('fill')).toBe('#E8EDF3'); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('stroke')).toBe('#123456'); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('lineWidth')).toBe(4); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('lineDash')).toEqual([4, 4]); + + ringGrogress.update({ + radius: 1, + innerRadius: 0.5, + width: 200, + height: 100, + percent: 0.4, + progressStyle, + autoFit: false, + }); + + expect(ringGrogress.chart.geometries[0].elements[0].getData().type).toBe('current'); + expect(ringGrogress.chart.geometries[0].elements[0].getData().percent).toBe(0.4); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('fill')).toBe('#FAAD14'); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('stroke')).toBe('#123456'); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('lineWidth')).toBe(4); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('lineDash')).toEqual([4, 4]); + expect(ringGrogress.chart.geometries[0].elements[1].getData().type).toBe('target'); + expect(ringGrogress.chart.geometries[0].elements[1].getData().percent).toBe(0.6); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('fill')).toBe('#E8EDF3'); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('stroke')).toBe('#654321'); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('lineWidth')).toBe(4); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('lineDash')).toEqual([4, 4]); + }); + + it('data without radius', () => { + const ringGrogress = new RingProgress(createDiv(), { + width: 200, + height: 100, + percent: 0.6, + autoFit: false, + }); + + ringGrogress.render(); + expect(ringGrogress.chart.geometries[0].coordinate.type).toBe('theta'); + expect(ringGrogress.chart.geometries[0].coordinate.radius).toBe(1); + expect(ringGrogress.chart.geometries[0].coordinate.innerRadius).toBe(0.8); + expect(ringGrogress.chart.geometries[0].elements[0].getData().type).toBe('current'); + expect(ringGrogress.chart.geometries[0].elements[0].getData().percent).toBe(0.6); + expect(ringGrogress.chart.geometries[0].elements[0].shape.attr('fill')).toBe('#FAAD14'); + expect(ringGrogress.chart.geometries[0].elements[1].getData().type).toBe('target'); + expect(ringGrogress.chart.geometries[0].elements[1].getData().percent).toBe(0.4); + expect(ringGrogress.chart.geometries[0].elements[1].shape.attr('fill')).toBe('#E8EDF3'); + }); +}); diff --git a/src/index.ts b/src/index.ts index fc620ac609..28cda4e10c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,3 +26,9 @@ export { TinyColumn, TinyColumnOptions } from './plots/tiny-column'; // 迷你面积图及类型定义 export { TinyArea, TinyAreaOptions } from './plots/tiny-area'; + +// 进度图及类型定义 +export { Progress, ProgressOptions } from './plots/progress'; + +// 环形进度图及类型定义 +export { RingProgress, RingProgressOptions } from './plots/ring-progress'; diff --git a/src/plots/progress/adaptor.ts b/src/plots/progress/adaptor.ts new file mode 100644 index 0000000000..f9f051e469 --- /dev/null +++ b/src/plots/progress/adaptor.ts @@ -0,0 +1,122 @@ +import { isFunction } from '@antv/util'; +import { Params } from '../../core/adaptor'; +import { flow } from '../../utils'; +import { ProgressOptions } from './types'; + +/** + * 字段 + * @param params + */ +function field(params: Params): Params { + const { chart, options } = params; + const { percent, color } = options; + + const data = [ + { + type: 'current', + percent: percent, + }, + { + type: 'target', + percent: 1 - percent, + }, + ]; + + chart.data(data); + + const geometry = chart.interval().position('1*percent').adjust('stack'); + const values = isFunction(color) ? color(percent) : color || ['#FAAD14', '#E8EDF3']; + + geometry.color('type', values); + + return params; +} + +/** + * meta 配置 + * @param params + */ +function meta(params: Params): Params { + const { chart, options } = params; + const { meta } = options; + + chart.scale(meta); + + return params; +} + +/** + * axis 配置 + * @param params + */ +function axis(params: Params): Params { + const { chart } = params; + + chart.axis(false); + + return params; +} + +/** + * legend 配置 + * @param params + */ +function legend(params: Params): Params { + const { chart } = params; + + chart.legend(false); + + return params; +} + +/** + * tooltip 配置 + * @param params + */ +function tooltip(params: Params): Params { + const { chart } = params; + + chart.tooltip(false); + + return params; +} + +/** + * 样式 + * @param params + */ +function style(params: Params): Params { + const { chart, options } = params; + const { progressStyle } = options; + + const geometry = chart.geometries[0]; + if (progressStyle && geometry) { + if (isFunction(progressStyle)) { + geometry.style('1*percent*type', progressStyle); + } else { + geometry.style(progressStyle); + } + } + return params; +} + +/** + * coordinate 配置 + * @param params + */ +function coordinate(params: Params): Params { + const { chart } = params; + + chart.coordinate('rect').transpose(); + + return params; +} + +/** + * 进度图适配器 + * @param chart + * @param options + */ +export function adaptor(params: Params) { + flow(field, meta, axis, legend, tooltip, style, coordinate)(params); +} diff --git a/src/plots/progress/index.ts b/src/plots/progress/index.ts new file mode 100644 index 0000000000..5b59179dd6 --- /dev/null +++ b/src/plots/progress/index.ts @@ -0,0 +1,18 @@ +import { Plot } from '../../core/plot'; +import { ProgressOptions } from './types'; +import { adaptor } from './adaptor'; +import { Adaptor } from '../../core/adaptor'; + +export { ProgressOptions }; + +export class Progress extends Plot { + /** 图表类型 */ + public type: string = 'process'; + + /** + * 获取 进度图 的适配器 + */ + protected getSchemaAdaptor(): Adaptor { + return adaptor; + } +} diff --git a/src/plots/progress/types.ts b/src/plots/progress/types.ts new file mode 100644 index 0000000000..1a86695091 --- /dev/null +++ b/src/plots/progress/types.ts @@ -0,0 +1,15 @@ +import { ChartOptions } from '../../types'; +import { ShapeStyle } from '../../types/style'; + +/** mini 图的配置继承自 ChartOptions,因为很多的 G2 图形配置都不需要 */ +export interface ProgressOptions extends ChartOptions { + // 通用数据配置 + /** 进度百分比 */ + readonly percent: number; + /** 进度条颜色 */ + readonly color?: string[] | ((percent: number) => string[]); + /** 数据字段元信息 */ + readonly meta?: Record; + /** 进度条样式 */ + readonly progressStyle?: ShapeStyle | ((x?: any, percent?: number, type?: string) => ShapeStyle); +} diff --git a/src/plots/ring-progress/adaptor.ts b/src/plots/ring-progress/adaptor.ts new file mode 100644 index 0000000000..df7d53f385 --- /dev/null +++ b/src/plots/ring-progress/adaptor.ts @@ -0,0 +1,126 @@ +import { isFunction } from '@antv/util'; +import { Params } from '../../core/adaptor'; +import { flow } from '../../utils'; +import { RingProgressOptions } from './types'; + +/** + * 字段 + * @param params + */ +function field(params: Params): Params { + const { chart, options } = params; + const { percent, color } = options; + + const data = [ + { + type: 'current', + percent: percent, + }, + { + type: 'target', + percent: 1 - percent, + }, + ]; + + chart.data(data); + + const geometry = chart.interval().position('1*percent').adjust('stack'); + const values = isFunction(color) ? color(percent) : color || ['#FAAD14', '#E8EDF3']; + + geometry.color('type', values); + + return params; +} + +/** + * meta 配置 + * @param params + */ +function meta(params: Params): Params { + const { chart, options } = params; + const { meta } = options; + + chart.scale(meta); + + return params; +} + +/** + * axis 配置 + * @param params + */ +function axis(params: Params): Params { + const { chart } = params; + + chart.axis(false); + + return params; +} + +/** + * legend 配置 + * @param params + */ +function legend(params: Params): Params { + const { chart } = params; + + chart.legend(false); + + return params; +} + +/** + * tooltip 配置 + * @param params + */ +function tooltip(params: Params): Params { + const { chart } = params; + + chart.tooltip(false); + + return params; +} + +/** + * 样式 + * @param params + */ +function style(params: Params): Params { + const { chart, options } = params; + const { progressStyle } = options; + + const geometry = chart.geometries[0]; + if (progressStyle && geometry) { + if (isFunction(progressStyle)) { + geometry.style('1*percent*type', progressStyle); + } else { + geometry.style(progressStyle); + } + } + return params; +} + +/** + * coordinate 配置 + * @param params + */ +function coordinate(params: Params): Params { + const { chart, options } = params; + const { innerRadius = 0.8, radius = 1 } = options; + + chart.coordinate('theta', { + innerRadius, + radius, + }); + + return params; +} + +/** + * 环形进度图适配器 + * @param chart + * @param options + */ +export function adaptor(params: Params) { + flow(field, meta, axis, legend, tooltip, style, coordinate)(params); +} diff --git a/src/plots/ring-progress/index.ts b/src/plots/ring-progress/index.ts new file mode 100644 index 0000000000..b180d92ec3 --- /dev/null +++ b/src/plots/ring-progress/index.ts @@ -0,0 +1,18 @@ +import { Plot } from '../../core/plot'; +import { RingProgressOptions } from './types'; +import { adaptor } from './adaptor'; +import { Adaptor } from '../../core/adaptor'; + +export { RingProgressOptions }; + +export class RingProgress extends Plot { + /** 图表类型 */ + public type: string = 'process'; + + /** + * 获取 环形进度图 的适配器 + */ + protected getSchemaAdaptor(): Adaptor { + return adaptor; + } +} diff --git a/src/plots/ring-progress/types.ts b/src/plots/ring-progress/types.ts new file mode 100644 index 0000000000..63e2ffe5d0 --- /dev/null +++ b/src/plots/ring-progress/types.ts @@ -0,0 +1,19 @@ +import { ChartOptions } from '../../types'; +import { ShapeStyle } from '../../types/style'; + +/** mini 图的配置继承自 ChartOptions,因为很多的 G2 图形配置都不需要 */ +export interface RingProgressOptions extends ChartOptions { + // 通用数据配置 + /** 进度百分比 */ + readonly percent: number; + /** 外环的半径 */ + readonly radius?: number; + /** 内环的半径 */ + readonly innerRadius?: number; + /** 进度条颜色 */ + readonly color?: string[] | ((percent: number) => string[]); + /** 数据字段元信息 */ + readonly meta?: Record; + /** 进度条样式 */ + readonly progressStyle?: ShapeStyle | ((x?: any, percent?: number, type?: string) => ShapeStyle); +} From 8da53b8558fa16d13d0234e15e8f4c05973b9283 Mon Sep 17 00:00:00 2001 From: kevin <31396322+lxfu1@users.noreply.github.com> Date: Thu, 23 Jul 2020 10:49:06 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E6=95=A3?= =?UTF-8?q?=E7=82=B9=E5=9B=BE=E5=8A=9F=E8=83=BD=E3=80=81=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B=20(#1322)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: scatter * feat: 完善散点图 adaptor 和添加部分单测 * feat: 完善散点图 adaptor 和添加部分单测 * 删除demos无用代码 * 删除 includeKeys * review 修改 * review 修改 * review 修复 * 去掉 size string 类型 * 修改test文案 * 修复 scatter 单测精度问题 * feat: scatter * feat: 完善散点图 adaptor 和添加部分单测 * feat: 完善散点图 adaptor 和添加部分单测 * 删除demos无用代码 * 删除 includeKeys * review 修改 * review 修改 * review 修复 * 去掉 size string 类型 * 修改test文案 * 修复 scatter 单测精度问题 * rebase Co-authored-by: liufu.lf --- __tests__/data/gender.ts | 509 +++++++++++++++++++ __tests__/unit/plots/scatter/axis-spec.ts | 40 ++ __tests__/unit/plots/scatter/color-spec.ts | 53 ++ __tests__/unit/plots/scatter/shape-spec.ts | 94 ++++ __tests__/unit/plots/scatter/size-spec.ts | 93 ++++ __tests__/unit/plots/scatter/style-spec.ts | 80 +++ __tests__/unit/plots/scatter/tooltip-spec.ts | 68 +++ docs/demos/scatter.md | 6 +- src/plots/scatter/adaptor.ts | 111 +++- src/plots/scatter/reflect.ts | 25 + src/plots/scatter/types.ts | 74 ++- 11 files changed, 1128 insertions(+), 25 deletions(-) create mode 100644 __tests__/data/gender.ts create mode 100644 __tests__/unit/plots/scatter/axis-spec.ts create mode 100644 __tests__/unit/plots/scatter/color-spec.ts create mode 100644 __tests__/unit/plots/scatter/shape-spec.ts create mode 100644 __tests__/unit/plots/scatter/size-spec.ts create mode 100644 __tests__/unit/plots/scatter/style-spec.ts create mode 100644 __tests__/unit/plots/scatter/tooltip-spec.ts create mode 100644 src/plots/scatter/reflect.ts diff --git a/__tests__/data/gender.ts b/__tests__/data/gender.ts new file mode 100644 index 0000000000..1af9365ca3 --- /dev/null +++ b/__tests__/data/gender.ts @@ -0,0 +1,509 @@ +export const data = [ + { gender: 'female', height: 161.2, weight: 51.6 }, + { gender: 'female', height: 167.5, weight: 59 }, + { gender: 'female', height: 159.5, weight: 49.2 }, + { gender: 'female', height: 157, weight: 63 }, + { gender: 'female', height: 155.8, weight: 53.6 }, + { gender: 'female', height: 170, weight: 59 }, + { gender: 'female', height: 159.1, weight: 47.6 }, + { gender: 'female', height: 166, weight: 69.8 }, + { gender: 'female', height: 176.2, weight: 66.8 }, + { gender: 'female', height: 160.2, weight: 75.2 }, + { gender: 'female', height: 172.5, weight: 55.2 }, + { gender: 'female', height: 170.9, weight: 54.2 }, + { gender: 'female', height: 172.9, weight: 62.5 }, + { gender: 'female', height: 153.4, weight: 42 }, + { gender: 'female', height: 160, weight: 50 }, + { gender: 'female', height: 147.2, weight: 49.8 }, + { gender: 'female', height: 168.2, weight: 49.2 }, + { gender: 'female', height: 175, weight: 73.2 }, + { gender: 'female', height: 157, weight: 47.8 }, + { gender: 'female', height: 167.6, weight: 68.8 }, + { gender: 'female', height: 159.5, weight: 50.6 }, + { gender: 'female', height: 175, weight: 82.5 }, + { gender: 'female', height: 166.8, weight: 57.2 }, + { gender: 'female', height: 176.5, weight: 87.8 }, + { gender: 'female', height: 170.2, weight: 72.8 }, + { gender: 'female', height: 174, weight: 54.5 }, + { gender: 'female', height: 173, weight: 59.8 }, + { gender: 'female', height: 179.9, weight: 67.3 }, + { gender: 'female', height: 170.5, weight: 67.8 }, + { gender: 'female', height: 160, weight: 47 }, + { gender: 'female', height: 154.4, weight: 46.2 }, + { gender: 'female', height: 162, weight: 55 }, + { gender: 'female', height: 176.5, weight: 83 }, + { gender: 'female', height: 160, weight: 54.4 }, + { gender: 'female', height: 152, weight: 45.8 }, + { gender: 'female', height: 162.1, weight: 53.6 }, + { gender: 'female', height: 170, weight: 73.2 }, + { gender: 'female', height: 160.2, weight: 52.1 }, + { gender: 'female', height: 161.3, weight: 67.9 }, + { gender: 'female', height: 166.4, weight: 56.6 }, + { gender: 'female', height: 168.9, weight: 62.3 }, + { gender: 'female', height: 163.8, weight: 58.5 }, + { gender: 'female', height: 167.6, weight: 54.5 }, + { gender: 'female', height: 160, weight: 50.2 }, + { gender: 'female', height: 161.3, weight: 60.3 }, + { gender: 'female', height: 167.6, weight: 58.3 }, + { gender: 'female', height: 165.1, weight: 56.2 }, + { gender: 'female', height: 160, weight: 50.2 }, + { gender: 'female', height: 170, weight: 72.9 }, + { gender: 'female', height: 157.5, weight: 59.8 }, + { gender: 'female', height: 167.6, weight: 61 }, + { gender: 'female', height: 160.7, weight: 69.1 }, + { gender: 'female', height: 163.2, weight: 55.9 }, + { gender: 'female', height: 152.4, weight: 46.5 }, + { gender: 'female', height: 157.5, weight: 54.3 }, + { gender: 'female', height: 168.3, weight: 54.8 }, + { gender: 'female', height: 180.3, weight: 60.7 }, + { gender: 'female', height: 165.5, weight: 60 }, + { gender: 'female', height: 165, weight: 62 }, + { gender: 'female', height: 164.5, weight: 60.3 }, + { gender: 'female', height: 156, weight: 52.7 }, + { gender: 'female', height: 160, weight: 74.3 }, + { gender: 'female', height: 163, weight: 62 }, + { gender: 'female', height: 165.7, weight: 73.1 }, + { gender: 'female', height: 161, weight: 80 }, + { gender: 'female', height: 162, weight: 54.7 }, + { gender: 'female', height: 166, weight: 53.2 }, + { gender: 'female', height: 174, weight: 75.7 }, + { gender: 'female', height: 172.7, weight: 61.1 }, + { gender: 'female', height: 167.6, weight: 55.7 }, + { gender: 'female', height: 151.1, weight: 48.7 }, + { gender: 'female', height: 164.5, weight: 52.3 }, + { gender: 'female', height: 163.5, weight: 50 }, + { gender: 'female', height: 152, weight: 59.3 }, + { gender: 'female', height: 169, weight: 62.5 }, + { gender: 'female', height: 164, weight: 55.7 }, + { gender: 'female', height: 161.2, weight: 54.8 }, + { gender: 'female', height: 155, weight: 45.9 }, + { gender: 'female', height: 170, weight: 70.6 }, + { gender: 'female', height: 176.2, weight: 67.2 }, + { gender: 'female', height: 170, weight: 69.4 }, + { gender: 'female', height: 162.5, weight: 58.2 }, + { gender: 'female', height: 170.3, weight: 64.8 }, + { gender: 'female', height: 164.1, weight: 71.6 }, + { gender: 'female', height: 169.5, weight: 52.8 }, + { gender: 'female', height: 163.2, weight: 59.8 }, + { gender: 'female', height: 154.5, weight: 49 }, + { gender: 'female', height: 159.8, weight: 50 }, + { gender: 'female', height: 173.2, weight: 69.2 }, + { gender: 'female', height: 170, weight: 55.9 }, + { gender: 'female', height: 161.4, weight: 63.4 }, + { gender: 'female', height: 169, weight: 58.2 }, + { gender: 'female', height: 166.2, weight: 58.6 }, + { gender: 'female', height: 159.4, weight: 45.7 }, + { gender: 'female', height: 162.5, weight: 52.2 }, + { gender: 'female', height: 159, weight: 48.6 }, + { gender: 'female', height: 162.8, weight: 57.8 }, + { gender: 'female', height: 159, weight: 55.6 }, + { gender: 'female', height: 179.8, weight: 66.8 }, + { gender: 'female', height: 162.9, weight: 59.4 }, + { gender: 'female', height: 161, weight: 53.6 }, + { gender: 'female', height: 151.1, weight: 73.2 }, + { gender: 'female', height: 168.2, weight: 53.4 }, + { gender: 'female', height: 168.9, weight: 69 }, + { gender: 'female', height: 173.2, weight: 58.4 }, + { gender: 'female', height: 171.8, weight: 56.2 }, + { gender: 'female', height: 178, weight: 70.6 }, + { gender: 'female', height: 164.3, weight: 59.8 }, + { gender: 'female', height: 163, weight: 72 }, + { gender: 'female', height: 168.5, weight: 65.2 }, + { gender: 'female', height: 166.8, weight: 56.6 }, + { gender: 'female', height: 172.7, weight: 105.2 }, + { gender: 'female', height: 163.5, weight: 51.8 }, + { gender: 'female', height: 169.4, weight: 63.4 }, + { gender: 'female', height: 167.8, weight: 59 }, + { gender: 'female', height: 159.5, weight: 47.6 }, + { gender: 'female', height: 167.6, weight: 63 }, + { gender: 'female', height: 161.2, weight: 55.2 }, + { gender: 'female', height: 160, weight: 45 }, + { gender: 'female', height: 163.2, weight: 54 }, + { gender: 'female', height: 162.2, weight: 50.2 }, + { gender: 'female', height: 161.3, weight: 60.2 }, + { gender: 'female', height: 149.5, weight: 44.8 }, + { gender: 'female', height: 157.5, weight: 58.8 }, + { gender: 'female', height: 163.2, weight: 56.4 }, + { gender: 'female', height: 172.7, weight: 62 }, + { gender: 'female', height: 155, weight: 49.2 }, + { gender: 'female', height: 156.5, weight: 67.2 }, + { gender: 'female', height: 164, weight: 53.8 }, + { gender: 'female', height: 160.9, weight: 54.4 }, + { gender: 'female', height: 162.8, weight: 58 }, + { gender: 'female', height: 167, weight: 59.8 }, + { gender: 'female', height: 160, weight: 54.8 }, + { gender: 'female', height: 160, weight: 43.2 }, + { gender: 'female', height: 168.9, weight: 60.5 }, + { gender: 'female', height: 158.2, weight: 46.4 }, + { gender: 'female', height: 156, weight: 64.4 }, + { gender: 'female', height: 160, weight: 48.8 }, + { gender: 'female', height: 167.1, weight: 62.2 }, + { gender: 'female', height: 158, weight: 55.5 }, + { gender: 'female', height: 167.6, weight: 57.8 }, + { gender: 'female', height: 156, weight: 54.6 }, + { gender: 'female', height: 162.1, weight: 59.2 }, + { gender: 'female', height: 173.4, weight: 52.7 }, + { gender: 'female', height: 159.8, weight: 53.2 }, + { gender: 'female', height: 170.5, weight: 64.5 }, + { gender: 'female', height: 159.2, weight: 51.8 }, + { gender: 'female', height: 157.5, weight: 56 }, + { gender: 'female', height: 161.3, weight: 63.6 }, + { gender: 'female', height: 162.6, weight: 63.2 }, + { gender: 'female', height: 160, weight: 59.5 }, + { gender: 'female', height: 168.9, weight: 56.8 }, + { gender: 'female', height: 165.1, weight: 64.1 }, + { gender: 'female', height: 162.6, weight: 50 }, + { gender: 'female', height: 165.1, weight: 72.3 }, + { gender: 'female', height: 166.4, weight: 55 }, + { gender: 'female', height: 160, weight: 55.9 }, + { gender: 'female', height: 152.4, weight: 60.4 }, + { gender: 'female', height: 170.2, weight: 69.1 }, + { gender: 'female', height: 162.6, weight: 84.5 }, + { gender: 'female', height: 170.2, weight: 55.9 }, + { gender: 'female', height: 158.8, weight: 55.5 }, + { gender: 'female', height: 172.7, weight: 69.5 }, + { gender: 'female', height: 167.6, weight: 76.4 }, + { gender: 'female', height: 162.6, weight: 61.4 }, + { gender: 'female', height: 167.6, weight: 65.9 }, + { gender: 'female', height: 156.2, weight: 58.6 }, + { gender: 'female', height: 175.2, weight: 66.8 }, + { gender: 'female', height: 172.1, weight: 56.6 }, + { gender: 'female', height: 162.6, weight: 58.6 }, + { gender: 'female', height: 160, weight: 55.9 }, + { gender: 'female', height: 165.1, weight: 59.1 }, + { gender: 'female', height: 182.9, weight: 81.8 }, + { gender: 'female', height: 166.4, weight: 70.7 }, + { gender: 'female', height: 165.1, weight: 56.8 }, + { gender: 'female', height: 177.8, weight: 60 }, + { gender: 'female', height: 165.1, weight: 58.2 }, + { gender: 'female', height: 175.3, weight: 72.7 }, + { gender: 'female', height: 154.9, weight: 54.1 }, + { gender: 'female', height: 158.8, weight: 49.1 }, + { gender: 'female', height: 172.7, weight: 75.9 }, + { gender: 'female', height: 168.9, weight: 55 }, + { gender: 'female', height: 161.3, weight: 57.3 }, + { gender: 'female', height: 167.6, weight: 55 }, + { gender: 'female', height: 165.1, weight: 65.5 }, + { gender: 'female', height: 175.3, weight: 65.5 }, + { gender: 'female', height: 157.5, weight: 48.6 }, + { gender: 'female', height: 163.8, weight: 58.6 }, + { gender: 'female', height: 167.6, weight: 63.6 }, + { gender: 'female', height: 165.1, weight: 55.2 }, + { gender: 'female', height: 165.1, weight: 62.7 }, + { gender: 'female', height: 168.9, weight: 56.6 }, + { gender: 'female', height: 162.6, weight: 53.9 }, + { gender: 'female', height: 164.5, weight: 63.2 }, + { gender: 'female', height: 176.5, weight: 73.6 }, + { gender: 'female', height: 168.9, weight: 62 }, + { gender: 'female', height: 175.3, weight: 63.6 }, + { gender: 'female', height: 159.4, weight: 53.2 }, + { gender: 'female', height: 160, weight: 53.4 }, + { gender: 'female', height: 170.2, weight: 55 }, + { gender: 'female', height: 162.6, weight: 70.5 }, + { gender: 'female', height: 167.6, weight: 54.5 }, + { gender: 'female', height: 162.6, weight: 54.5 }, + { gender: 'female', height: 160.7, weight: 55.9 }, + { gender: 'female', height: 160, weight: 59 }, + { gender: 'female', height: 157.5, weight: 63.6 }, + { gender: 'female', height: 162.6, weight: 54.5 }, + { gender: 'female', height: 152.4, weight: 47.3 }, + { gender: 'female', height: 170.2, weight: 67.7 }, + { gender: 'female', height: 165.1, weight: 80.9 }, + { gender: 'female', height: 172.7, weight: 70.5 }, + { gender: 'female', height: 165.1, weight: 60.9 }, + { gender: 'female', height: 170.2, weight: 63.6 }, + { gender: 'female', height: 170.2, weight: 54.5 }, + { gender: 'female', height: 170.2, weight: 59.1 }, + { gender: 'female', height: 161.3, weight: 70.5 }, + { gender: 'female', height: 167.6, weight: 52.7 }, + { gender: 'female', height: 167.6, weight: 62.7 }, + { gender: 'female', height: 165.1, weight: 86.3 }, + { gender: 'female', height: 162.6, weight: 66.4 }, + { gender: 'female', height: 152.4, weight: 67.3 }, + { gender: 'female', height: 168.9, weight: 63 }, + { gender: 'female', height: 170.2, weight: 73.6 }, + { gender: 'female', height: 175.2, weight: 62.3 }, + { gender: 'female', height: 175.2, weight: 57.7 }, + { gender: 'female', height: 160, weight: 55.4 }, + { gender: 'female', height: 165.1, weight: 104.1 }, + { gender: 'female', height: 174, weight: 55.5 }, + { gender: 'female', height: 170.2, weight: 77.3 }, + { gender: 'female', height: 160, weight: 80.5 }, + { gender: 'female', height: 167.6, weight: 64.5 }, + { gender: 'female', height: 167.6, weight: 72.3 }, + { gender: 'female', height: 167.6, weight: 61.4 }, + { gender: 'female', height: 154.9, weight: 58.2 }, + { gender: 'female', height: 162.6, weight: 81.8 }, + { gender: 'female', height: 175.3, weight: 63.6 }, + { gender: 'female', height: 171.4, weight: 53.4 }, + { gender: 'female', height: 157.5, weight: 54.5 }, + { gender: 'female', height: 165.1, weight: 53.6 }, + { gender: 'female', height: 160, weight: 60 }, + { gender: 'female', height: 174, weight: 73.6 }, + { gender: 'female', height: 162.6, weight: 61.4 }, + { gender: 'female', height: 174, weight: 55.5 }, + { gender: 'female', height: 162.6, weight: 63.6 }, + { gender: 'female', height: 161.3, weight: 60.9 }, + { gender: 'female', height: 156.2, weight: 60 }, + { gender: 'female', height: 149.9, weight: 46.8 }, + { gender: 'female', height: 169.5, weight: 57.3 }, + { gender: 'female', height: 160, weight: 64.1 }, + { gender: 'female', height: 175.3, weight: 63.6 }, + { gender: 'female', height: 169.5, weight: 67.3 }, + { gender: 'female', height: 160, weight: 75.5 }, + { gender: 'female', height: 172.7, weight: 68.2 }, + { gender: 'female', height: 162.6, weight: 61.4 }, + { gender: 'female', height: 157.5, weight: 76.8 }, + { gender: 'female', height: 176.5, weight: 71.8 }, + { gender: 'female', height: 164.4, weight: 55.5 }, + { gender: 'female', height: 160.7, weight: 48.6 }, + { gender: 'female', height: 174, weight: 66.4 }, + { gender: 'female', height: 163.8, weight: 67.3 }, + { gender: 'male', height: 174, weight: 65.6 }, + { gender: 'male', height: 175.3, weight: 71.8 }, + { gender: 'male', height: 193.5, weight: 80.7 }, + { gender: 'male', height: 186.5, weight: 72.6 }, + { gender: 'male', height: 187.2, weight: 78.8 }, + { gender: 'male', height: 181.5, weight: 74.8 }, + { gender: 'male', height: 184, weight: 86.4 }, + { gender: 'male', height: 184.5, weight: 78.4 }, + { gender: 'male', height: 175, weight: 62 }, + { gender: 'male', height: 184, weight: 81.6 }, + { gender: 'male', height: 180, weight: 76.6 }, + { gender: 'male', height: 177.8, weight: 83.6 }, + { gender: 'male', height: 192, weight: 90 }, + { gender: 'male', height: 176, weight: 74.6 }, + { gender: 'male', height: 174, weight: 71 }, + { gender: 'male', height: 184, weight: 79.6 }, + { gender: 'male', height: 192.7, weight: 93.8 }, + { gender: 'male', height: 171.5, weight: 70 }, + { gender: 'male', height: 173, weight: 72.4 }, + { gender: 'male', height: 176, weight: 85.9 }, + { gender: 'male', height: 176, weight: 78.8 }, + { gender: 'male', height: 180.5, weight: 77.8 }, + { gender: 'male', height: 172.7, weight: 66.2 }, + { gender: 'male', height: 176, weight: 86.4 }, + { gender: 'male', height: 173.5, weight: 81.8 }, + { gender: 'male', height: 178, weight: 89.6 }, + { gender: 'male', height: 180.3, weight: 82.8 }, + { gender: 'male', height: 180.3, weight: 76.4 }, + { gender: 'male', height: 164.5, weight: 63.2 }, + { gender: 'male', height: 173, weight: 60.9 }, + { gender: 'male', height: 183.5, weight: 74.8 }, + { gender: 'male', height: 175.5, weight: 70 }, + { gender: 'male', height: 188, weight: 72.4 }, + { gender: 'male', height: 189.2, weight: 84.1 }, + { gender: 'male', height: 172.8, weight: 69.1 }, + { gender: 'male', height: 170, weight: 59.5 }, + { gender: 'male', height: 182, weight: 67.2 }, + { gender: 'male', height: 170, weight: 61.3 }, + { gender: 'male', height: 177.8, weight: 68.6 }, + { gender: 'male', height: 184.2, weight: 80.1 }, + { gender: 'male', height: 186.7, weight: 87.8 }, + { gender: 'male', height: 171.4, weight: 84.7 }, + { gender: 'male', height: 172.7, weight: 73.4 }, + { gender: 'male', height: 175.3, weight: 72.1 }, + { gender: 'male', height: 180.3, weight: 82.6 }, + { gender: 'male', height: 182.9, weight: 88.7 }, + { gender: 'male', height: 188, weight: 84.1 }, + { gender: 'male', height: 177.2, weight: 94.1 }, + { gender: 'male', height: 172.1, weight: 74.9 }, + { gender: 'male', height: 167, weight: 59.1 }, + { gender: 'male', height: 169.5, weight: 75.6 }, + { gender: 'male', height: 174, weight: 86.2 }, + { gender: 'male', height: 172.7, weight: 75.3 }, + { gender: 'male', height: 182.2, weight: 87.1 }, + { gender: 'male', height: 164.1, weight: 55.2 }, + { gender: 'male', height: 163, weight: 57 }, + { gender: 'male', height: 171.5, weight: 61.4 }, + { gender: 'male', height: 184.2, weight: 76.8 }, + { gender: 'male', height: 174, weight: 86.8 }, + { gender: 'male', height: 174, weight: 72.2 }, + { gender: 'male', height: 177, weight: 71.6 }, + { gender: 'male', height: 186, weight: 84.8 }, + { gender: 'male', height: 167, weight: 68.2 }, + { gender: 'male', height: 171.8, weight: 66.1 }, + { gender: 'male', height: 182, weight: 72 }, + { gender: 'male', height: 167, weight: 64.6 }, + { gender: 'male', height: 177.8, weight: 74.8 }, + { gender: 'male', height: 164.5, weight: 70 }, + { gender: 'male', height: 192, weight: 101.6 }, + { gender: 'male', height: 175.5, weight: 63.2 }, + { gender: 'male', height: 171.2, weight: 79.1 }, + { gender: 'male', height: 181.6, weight: 78.9 }, + { gender: 'male', height: 167.4, weight: 67.7 }, + { gender: 'male', height: 181.1, weight: 66 }, + { gender: 'male', height: 177, weight: 68.2 }, + { gender: 'male', height: 174.5, weight: 63.9 }, + { gender: 'male', height: 177.5, weight: 72 }, + { gender: 'male', height: 170.5, weight: 56.8 }, + { gender: 'male', height: 182.4, weight: 74.5 }, + { gender: 'male', height: 197.1, weight: 90.9 }, + { gender: 'male', height: 180.1, weight: 93 }, + { gender: 'male', height: 175.5, weight: 80.9 }, + { gender: 'male', height: 180.6, weight: 72.7 }, + { gender: 'male', height: 184.4, weight: 68 }, + { gender: 'male', height: 175.5, weight: 70.9 }, + { gender: 'male', height: 180.6, weight: 72.5 }, + { gender: 'male', height: 177, weight: 72.5 }, + { gender: 'male', height: 177.1, weight: 83.4 }, + { gender: 'male', height: 181.6, weight: 75.5 }, + { gender: 'male', height: 176.5, weight: 73 }, + { gender: 'male', height: 175, weight: 70.2 }, + { gender: 'male', height: 174, weight: 73.4 }, + { gender: 'male', height: 165.1, weight: 70.5 }, + { gender: 'male', height: 177, weight: 68.9 }, + { gender: 'male', height: 192, weight: 102.3 }, + { gender: 'male', height: 176.5, weight: 68.4 }, + { gender: 'male', height: 169.4, weight: 65.9 }, + { gender: 'male', height: 182.1, weight: 75.7 }, + { gender: 'male', height: 179.8, weight: 84.5 }, + { gender: 'male', height: 175.3, weight: 87.7 }, + { gender: 'male', height: 184.9, weight: 86.4 }, + { gender: 'male', height: 177.3, weight: 73.2 }, + { gender: 'male', height: 167.4, weight: 53.9 }, + { gender: 'male', height: 178.1, weight: 72 }, + { gender: 'male', height: 168.9, weight: 55.5 }, + { gender: 'male', height: 157.2, weight: 58.4 }, + { gender: 'male', height: 180.3, weight: 83.2 }, + { gender: 'male', height: 170.2, weight: 72.7 }, + { gender: 'male', height: 177.8, weight: 64.1 }, + { gender: 'male', height: 172.7, weight: 72.3 }, + { gender: 'male', height: 165.1, weight: 65 }, + { gender: 'male', height: 186.7, weight: 86.4 }, + { gender: 'male', height: 165.1, weight: 65 }, + { gender: 'male', height: 174, weight: 88.6 }, + { gender: 'male', height: 175.3, weight: 84.1 }, + { gender: 'male', height: 185.4, weight: 66.8 }, + { gender: 'male', height: 177.8, weight: 75.5 }, + { gender: 'male', height: 180.3, weight: 93.2 }, + { gender: 'male', height: 180.3, weight: 82.7 }, + { gender: 'male', height: 177.8, weight: 58 }, + { gender: 'male', height: 177.8, weight: 79.5 }, + { gender: 'male', height: 177.8, weight: 78.6 }, + { gender: 'male', height: 177.8, weight: 71.8 }, + { gender: 'male', height: 177.8, weight: 116.4 }, + { gender: 'male', height: 163.8, weight: 72.2 }, + { gender: 'male', height: 188, weight: 83.6 }, + { gender: 'male', height: 198.1, weight: 85.5 }, + { gender: 'male', height: 175.3, weight: 90.9 }, + { gender: 'male', height: 166.4, weight: 85.9 }, + { gender: 'male', height: 190.5, weight: 89.1 }, + { gender: 'male', height: 166.4, weight: 75 }, + { gender: 'male', height: 177.8, weight: 77.7 }, + { gender: 'male', height: 179.7, weight: 86.4 }, + { gender: 'male', height: 172.7, weight: 90.9 }, + { gender: 'male', height: 190.5, weight: 73.6 }, + { gender: 'male', height: 185.4, weight: 76.4 }, + { gender: 'male', height: 168.9, weight: 69.1 }, + { gender: 'male', height: 167.6, weight: 84.5 }, + { gender: 'male', height: 175.3, weight: 64.5 }, + { gender: 'male', height: 170.2, weight: 69.1 }, + { gender: 'male', height: 190.5, weight: 108.6 }, + { gender: 'male', height: 177.8, weight: 86.4 }, + { gender: 'male', height: 190.5, weight: 80.9 }, + { gender: 'male', height: 177.8, weight: 87.7 }, + { gender: 'male', height: 184.2, weight: 94.5 }, + { gender: 'male', height: 176.5, weight: 80.2 }, + { gender: 'male', height: 177.8, weight: 72 }, + { gender: 'male', height: 180.3, weight: 71.4 }, + { gender: 'male', height: 171.4, weight: 72.7 }, + { gender: 'male', height: 172.7, weight: 84.1 }, + { gender: 'male', height: 172.7, weight: 76.8 }, + { gender: 'male', height: 177.8, weight: 63.6 }, + { gender: 'male', height: 177.8, weight: 80.9 }, + { gender: 'male', height: 182.9, weight: 80.9 }, + { gender: 'male', height: 170.2, weight: 85.5 }, + { gender: 'male', height: 167.6, weight: 68.6 }, + { gender: 'male', height: 175.3, weight: 67.7 }, + { gender: 'male', height: 165.1, weight: 66.4 }, + { gender: 'male', height: 185.4, weight: 102.3 }, + { gender: 'male', height: 181.6, weight: 70.5 }, + { gender: 'male', height: 172.7, weight: 95.9 }, + { gender: 'male', height: 190.5, weight: 84.1 }, + { gender: 'male', height: 179.1, weight: 87.3 }, + { gender: 'male', height: 175.3, weight: 71.8 }, + { gender: 'male', height: 170.2, weight: 65.9 }, + { gender: 'male', height: 193, weight: 95.9 }, + { gender: 'male', height: 171.4, weight: 91.4 }, + { gender: 'male', height: 177.8, weight: 81.8 }, + { gender: 'male', height: 177.8, weight: 96.8 }, + { gender: 'male', height: 167.6, weight: 69.1 }, + { gender: 'male', height: 167.6, weight: 82.7 }, + { gender: 'male', height: 180.3, weight: 75.5 }, + { gender: 'male', height: 182.9, weight: 79.5 }, + { gender: 'male', height: 176.5, weight: 73.6 }, + { gender: 'male', height: 186.7, weight: 91.8 }, + { gender: 'male', height: 188, weight: 84.1 }, + { gender: 'male', height: 188, weight: 85.9 }, + { gender: 'male', height: 177.8, weight: 81.8 }, + { gender: 'male', height: 174, weight: 82.5 }, + { gender: 'male', height: 177.8, weight: 80.5 }, + { gender: 'male', height: 171.4, weight: 70 }, + { gender: 'male', height: 185.4, weight: 81.8 }, + { gender: 'male', height: 185.4, weight: 84.1 }, + { gender: 'male', height: 188, weight: 90.5 }, + { gender: 'male', height: 188, weight: 91.4 }, + { gender: 'male', height: 182.9, weight: 89.1 }, + { gender: 'male', height: 176.5, weight: 85 }, + { gender: 'male', height: 175.3, weight: 69.1 }, + { gender: 'male', height: 175.3, weight: 73.6 }, + { gender: 'male', height: 188, weight: 80.5 }, + { gender: 'male', height: 188, weight: 82.7 }, + { gender: 'male', height: 175.3, weight: 86.4 }, + { gender: 'male', height: 170.5, weight: 67.7 }, + { gender: 'male', height: 179.1, weight: 92.7 }, + { gender: 'male', height: 177.8, weight: 93.6 }, + { gender: 'male', height: 175.3, weight: 70.9 }, + { gender: 'male', height: 182.9, weight: 75 }, + { gender: 'male', height: 170.8, weight: 93.2 }, + { gender: 'male', height: 188, weight: 93.2 }, + { gender: 'male', height: 180.3, weight: 77.7 }, + { gender: 'male', height: 177.8, weight: 61.4 }, + { gender: 'male', height: 185.4, weight: 94.1 }, + { gender: 'male', height: 168.9, weight: 75 }, + { gender: 'male', height: 185.4, weight: 83.6 }, + { gender: 'male', height: 180.3, weight: 85.5 }, + { gender: 'male', height: 174, weight: 73.9 }, + { gender: 'male', height: 167.6, weight: 66.8 }, + { gender: 'male', height: 182.9, weight: 87.3 }, + { gender: 'male', height: 160, weight: 72.3 }, + { gender: 'male', height: 180.3, weight: 88.6 }, + { gender: 'male', height: 167.6, weight: 75.5 }, + { gender: 'male', height: 186.7, weight: 101.4 }, + { gender: 'male', height: 175.3, weight: 91.1 }, + { gender: 'male', height: 175.3, weight: 67.3 }, + { gender: 'male', height: 175.9, weight: 77.7 }, + { gender: 'male', height: 175.3, weight: 81.8 }, + { gender: 'male', height: 179.1, weight: 75.5 }, + { gender: 'male', height: 181.6, weight: 84.5 }, + { gender: 'male', height: 177.8, weight: 76.6 }, + { gender: 'male', height: 182.9, weight: 85 }, + { gender: 'male', height: 177.8, weight: 102.5 }, + { gender: 'male', height: 184.2, weight: 77.3 }, + { gender: 'male', height: 179.1, weight: 71.8 }, + { gender: 'male', height: 176.5, weight: 87.9 }, + { gender: 'male', height: 188, weight: 94.3 }, + { gender: 'male', height: 174, weight: 70.9 }, + { gender: 'male', height: 167.6, weight: 64.5 }, + { gender: 'male', height: 170.2, weight: 77.3 }, + { gender: 'male', height: 167.6, weight: 72.3 }, + { gender: 'male', height: 188, weight: 87.3 }, + { gender: 'male', height: 174, weight: 80 }, + { gender: 'male', height: 176.5, weight: 82.3 }, + { gender: 'male', height: 180.3, weight: 73.6 }, + { gender: 'male', height: 167.6, weight: 74.1 }, + { gender: 'male', height: 188, weight: 85.9 }, + { gender: 'male', height: 180.3, weight: 73.2 }, + { gender: 'male', height: 167.6, weight: 76.3 }, + { gender: 'male', height: 183, weight: 65.9 }, + { gender: 'male', height: 183, weight: 90.9 }, + { gender: 'male', height: 179.1, weight: 89.1 }, + { gender: 'male', height: 170.2, weight: 62.3 }, + { gender: 'male', height: 177.8, weight: 82.7 }, + { gender: 'male', height: 179.1, weight: 79.1 }, + { gender: 'male', height: 190.5, weight: 98.2 }, + { gender: 'male', height: 177.8, weight: 84.1 }, + { gender: 'male', height: 180.3, weight: 83.2 }, + { gender: 'male', height: 180.3, weight: 83.2 }, +]; diff --git a/__tests__/unit/plots/scatter/axis-spec.ts b/__tests__/unit/plots/scatter/axis-spec.ts new file mode 100644 index 0000000000..dd19bcf433 --- /dev/null +++ b/__tests__/unit/plots/scatter/axis-spec.ts @@ -0,0 +1,40 @@ +import { Scatter } from '../../../../src'; +import { data } from '../../../data/gender'; +import { createDiv } from '../../../utils/dom'; + +describe('scatter', () => { + it('axis: axis options', () => { + const scatter = new Scatter(createDiv(), { + width: 400, + height: 300, + appendPadding: 10, + data, + xField: 'weight', + yField: 'height', + shape: ['circle', 'square'], + xAxis: { + nice: true, + label: { + formatter: (text: string, item: any, index: number) => { + return text; + }, + style: { + fill: 'green', + fontSize: 16, + }, + }, + }, + }); + + scatter.render(); + + const geometry = scatter.chart.geometries[0]; + const elements = geometry.elements; + + expect(elements.length).toBe(507); + // @ts-ignore + expect(scatter.chart.options.axes.weight.label.style.fill).toBe('green'); + // @ts-ignore + expect(scatter.chart.options.axes.weight.label.style.fontSize).toBe(16); + }); +}); diff --git a/__tests__/unit/plots/scatter/color-spec.ts b/__tests__/unit/plots/scatter/color-spec.ts new file mode 100644 index 0000000000..e23b69af15 --- /dev/null +++ b/__tests__/unit/plots/scatter/color-spec.ts @@ -0,0 +1,53 @@ +import { Scatter } from '../../../../src'; +import { data } from '../../../data/gender'; +import { createDiv } from '../../../utils/dom'; + +describe('scatter', () => { + it('color: string options', () => { + const scatter = new Scatter(createDiv(), { + width: 400, + height: 300, + appendPadding: 10, + data, + xField: 'weight', + yField: 'height', + color: 'red', + xAxis: { + nice: true, + }, + }); + + scatter.render(); + + const geometry = scatter.chart.geometries[0]; + const elements = geometry.elements; + + expect(elements.length).toBe(507); + expect(elements[0].getModel().color).toBe('red'); + }); + + it('color: string array options', () => { + const scatter = new Scatter(createDiv(), { + width: 400, + height: 300, + appendPadding: 10, + data, + xField: 'weight', + yField: 'height', + color: ['#e764ff', '#2b0033'], + xAxis: { + nice: true, + }, + }); + + scatter.render(); + + const geometry = scatter.chart.geometries[0]; + const elements = geometry.elements; + + // @ts-ignore + expect(geometry.attributeOption.color.values.length).toBe(2); + expect(elements.length).toBe(507); + expect(elements[0].getModel().color).not.toBe('red'); + }); +}); diff --git a/__tests__/unit/plots/scatter/shape-spec.ts b/__tests__/unit/plots/scatter/shape-spec.ts new file mode 100644 index 0000000000..05ce508e28 --- /dev/null +++ b/__tests__/unit/plots/scatter/shape-spec.ts @@ -0,0 +1,94 @@ +import { Scatter } from '../../../../src'; +import { data } from '../../../data/gender'; +import { createDiv } from '../../../utils/dom'; + +describe('scatter', () => { + it('shpae: string options', () => { + const scatter = new Scatter(createDiv(), { + width: 400, + height: 300, + appendPadding: 10, + data, + xField: 'weight', + yField: 'height', + shape: 'hollow-diamond', + xAxis: { + nice: true, + }, + }); + + scatter.render(); + + const geometry = scatter.chart.geometries[0]; + const elements = geometry.elements; + + expect(elements.length).toBe(507); + expect(elements[0].getModel().shape).toBe('hollow-diamond'); + expect(elements[elements.length - 1].getModel().shape).toBe('hollow-diamond'); + }); + + it('shape: string array options', () => { + const scatter = new Scatter(createDiv(), { + width: 400, + height: 300, + appendPadding: 10, + data, + xField: 'weight', + yField: 'height', + shape: ['circle', 'square'], + xAxis: { + nice: true, + }, + }); + + scatter.render(); + const geometry = scatter.chart.geometries[0]; + const elements = geometry.elements; + const shapeArr = []; + elements.forEach((ele) => { + shapeArr.push(ele.getModel().shape); + }); + const set = new Set(shapeArr); + + // @ts-ignore + expect(geometry.attributeOption.shape.values.length).toBe(2); + expect(elements.length).toBe(507); + expect(set.size).toBe(2); + }); + + it('shape: callback options', () => { + const scatter = new Scatter(createDiv(), { + width: 400, + height: 300, + appendPadding: 10, + data, + xField: 'weight', + yField: 'height', + shapeField: 'gender', + size: 10, + shape: (gender: string) => { + if (gender === 'female') { + return 'square'; + } + return 'circle'; + }, + xAxis: { + nice: true, + }, + }); + + scatter.render(); + const geometry = scatter.chart.geometries[0]; + const elements = geometry.elements; + const shapeArr = []; + elements.forEach((ele) => { + shapeArr.push(ele.getModel().shape); + }); + + // @ts-ignore + expect(geometry.attributeOption.shape.callback).toBeFunction(); + expect(elements.length).toBe(507); + expect(shapeArr).toContain('circle'); + expect(shapeArr).toContain('square'); + }); +}); diff --git a/__tests__/unit/plots/scatter/size-spec.ts b/__tests__/unit/plots/scatter/size-spec.ts new file mode 100644 index 0000000000..5fc07fab8e --- /dev/null +++ b/__tests__/unit/plots/scatter/size-spec.ts @@ -0,0 +1,93 @@ +import { Scatter } from '../../../../src'; +import { data } from '../../../data/gender'; +import { createDiv } from '../../../utils/dom'; + +describe('scatter', () => { + it('size: number options', () => { + const scatter = new Scatter(createDiv(), { + width: 400, + height: 300, + appendPadding: 10, + data, + xField: 'weight', + yField: 'height', + sizeField: 'weight', + size: [5, 10], + xAxis: { + nice: true, + }, + }); + + scatter.render(); + + const geometry = scatter.chart.geometries[0]; + const elements = geometry.elements; + const sizeArr = []; + elements.forEach((ele) => { + sizeArr.push(ele.getModel().size); + }); + sizeArr.sort((a, b) => a - b); + + expect(elements.length).toBe(507); + expect(Math.floor(sizeArr[0])).toBe(5); + expect(sizeArr[0]).not.toEqual(sizeArr[sizeArr.length - 1]); + }); + + it('size: number array options', () => { + const scatter = new Scatter(createDiv(), { + width: 400, + height: 300, + appendPadding: 10, + data, + xField: 'weight', + yField: 'height', + size: [5, 10], + xAxis: { + nice: true, + }, + }); + + scatter.render(); + + const geometry = scatter.chart.geometries[0]; + const elements = geometry.elements; + + // @ts-ignore + expect(geometry.attributeOption.size.values.length).toBe(2); + expect(elements.length).toBe(507); + expect(elements[0].getModel().size).not.toBe(elements[elements.length - 1].getModel().size); + }); + + it('size: callback options', () => { + const scatter = new Scatter(createDiv(), { + width: 400, + height: 300, + appendPadding: 10, + data, + xField: 'weight', + yField: 'height', + size: (value: number) => { + return Math.ceil(value / 10); + }, + xAxis: { + nice: true, + }, + }); + + scatter.render(); + + const geometry = scatter.chart.geometries[0]; + const elements = geometry.elements; + const sizeArr = []; + elements.forEach((ele) => { + sizeArr.push(ele.getModel().size); + }); + sizeArr.sort((a, b) => a - b); + + // @ts-ignore + expect(geometry.attributeOption.size.callback).toBeFunction(); + expect(elements.length).toBe(507); + expect(sizeArr[0] > 0).toBeTruthy(); + expect(sizeArr[0]).not.toEqual(sizeArr[sizeArr.length - 1]); + }); +}); diff --git a/__tests__/unit/plots/scatter/style-spec.ts b/__tests__/unit/plots/scatter/style-spec.ts new file mode 100644 index 0000000000..0b022bdcaa --- /dev/null +++ b/__tests__/unit/plots/scatter/style-spec.ts @@ -0,0 +1,80 @@ +import { Scatter } from '../../../../src'; +import { data } from '../../../data/gender'; +import { createDiv } from '../../../utils/dom'; + +describe('scatter', () => { + it('style: object options', () => { + const scatter = new Scatter(createDiv(), { + width: 400, + height: 300, + appendPadding: 10, + data, + xField: 'weight', + yField: 'height', + sizeField: 'weight', + size: [5, 10], + xAxis: { + nice: true, + }, + pointStyle: { + fill: 'red', + stroke: 'yellow', + opacity: 0.8, + }, + }); + + scatter.render(); + + const geometry = scatter.chart.geometries[0]; + const elements = geometry.elements; + + expect(elements[0].shape.attr('fill')).toBe('red'); + expect(elements[0].shape.attr('stroke')).toBe('yellow'); + expect(elements[0].shape.attr('opacity')).toBe(0.8); + }); + + it('style: callback options', () => { + const scatter = new Scatter(createDiv(), { + width: 400, + height: 300, + appendPadding: 10, + data, + xField: 'weight', + yField: 'height', + sizeField: 'weight', + size: [5, 10], + colorField: 'gender', + xAxis: { + nice: true, + }, + pointStyle: (x: number, y: number, colorField: string) => { + if (colorField === 'male') { + return { + fill: 'green', + stroke: 'yellow', + opacity: 0.8, + }; + } + return { + fill: 'red', + stroke: 'yellow', + opacity: 0.8, + }; + }, + }); + + scatter.render(); + + const geometry = scatter.chart.geometries[0]; + const elements = geometry.elements; + const colorArr = []; + elements.forEach((ele) => { + colorArr.push(ele.shape.attr('fill')); + }); + + expect(colorArr).toContain('red'); + expect(colorArr).toContain('green'); + expect(elements[0].shape.attr('stroke')).toBe('yellow'); + expect(elements[0].shape.attr('opacity')).toBe(0.8); + }); +}); diff --git a/__tests__/unit/plots/scatter/tooltip-spec.ts b/__tests__/unit/plots/scatter/tooltip-spec.ts new file mode 100644 index 0000000000..146a21ff09 --- /dev/null +++ b/__tests__/unit/plots/scatter/tooltip-spec.ts @@ -0,0 +1,68 @@ +import { Scatter } from '../../../../src'; +import { data } from '../../../data/gender'; +import { createDiv } from '../../../utils/dom'; + +describe('scatter', () => { + it('tooltip: title options', () => { + const scatter = new Scatter(createDiv(), { + width: 400, + height: 300, + data, + xField: 'weight', + yField: 'height', + size: 10, + xAxis: { + nice: true, + }, + tooltip: { + title: 'scatter', + }, + }); + + scatter.render(); + + // @ts-ignore + expect(scatter.chart.options.tooltip.title).toBe('scatter'); + scatter.update({ + ...scatter.options, + tooltip: false, + }); + // @ts-ignore + expect(scatter.chart.options.tooltip).toBe(false); + expect(scatter.chart.getComponents().find((co) => co.type === 'tooltip')).toBe(undefined); + }); + + it('tooltip: itemTpl options', () => { + const scatter = new Scatter(createDiv(), { + width: 400, + height: 300, + data, + xField: 'weight', + yField: 'height', + size: 10, + xAxis: { + nice: true, + }, + tooltip: { + title: 'scatter', + showTitle: false, + itemTpl: + '
  • ' + + '' + + '{name}: ' + + '{value}' + + '
  • ', + }, + }); + + scatter.render(); + + const geometry = scatter.chart.geometries[0]; + const elements = geometry.elements; + const bbox = elements[elements.length - 1].getBBox(); + + // 正常渲染 + scatter.chart.showTooltip({ x: bbox.maxX, y: bbox.maxY }); + expect(document.getElementsByClassName('itemTpl')[0].innerHTML).not.toBeNull(); + }); +}); diff --git a/docs/demos/scatter.md b/docs/demos/scatter.md index 0c94d767df..356ddf1cac 100644 --- a/docs/demos/scatter.md +++ b/docs/demos/scatter.md @@ -28,12 +28,12 @@ const Page: React.FC = () => { width: 400, height: 300, appendPadding: 10, - data, + data: data.slice(0, 50), xField: 'weight', yField: 'height', - seriesField: 'gender', shape: ['circle', 'square'], - symbolSize: 5, + color: ['green', 'red'], + size: 'weight', tooltip: { showCrosshairs: true, crosshairs: { diff --git a/src/plots/scatter/adaptor.ts b/src/plots/scatter/adaptor.ts index 9465f5a361..e87eb3365b 100644 --- a/src/plots/scatter/adaptor.ts +++ b/src/plots/scatter/adaptor.ts @@ -1,7 +1,11 @@ +import { deepMix, isFunction } from '@antv/util'; import { Params } from '../../core/adaptor'; -import { flow } from '../../utils'; +import { flow, pick } from '../../utils'; import { ScatterOptions } from './types'; import { tooltip } from '../../common/adaptor'; +import { findGeometry } from '../../common/helper'; +import { AXIS_META_CONFIG_KEYS } from '../../constant'; +import { REFLECTS } from './reflect'; /** * 字段 @@ -9,23 +13,31 @@ import { tooltip } from '../../common/adaptor'; */ function field(params: Params): Params { const { chart, options } = params; - const { data, xField, yField, seriesField, color, symbolSize, shape } = options; + const { data, xField, yField, seriesField } = options; // 散点图操作逻辑 chart.data(data); const geometry = chart.point().position(`${xField}*${yField}`); - // 颜色映射 - if (seriesField) { - geometry.color(seriesField, color); - geometry.shape(seriesField, shape); - } + // 统一处理 color、 size、 shape + const reflectKeys = Object.keys(REFLECTS); + reflectKeys.forEach((key: string) => { + if (options[key]) { + let validateRules = false; + (REFLECTS[key].rules || []).forEach((fn: (arg: any) => boolean) => { + // 满足任一规则即可 + if (fn && fn(options[key])) { + validateRules = true; + } + }); + if (validateRules) { + geometry[REFLECTS[key].action](options[REFLECTS[key].field] || seriesField || xField, options[key]); + } else { + geometry[REFLECTS[key].action](options[key]); + } + } + }); - // 大小映射 - if (symbolSize) { - const size = typeof symbolSize === 'number' ? ([symbolSize, symbolSize] as [number, number]) : symbolSize; - geometry.size(yField, size); - } return params; } @@ -34,7 +46,17 @@ function field(params: Params): Params { * @param params */ function meta(params: Params): Params { - // TODO + const { chart, options } = params; + const { meta, xAxis, yAxis, xField, yField } = options; + + // meta 直接是 scale 的信息 + const scales = deepMix({}, meta, { + [xField]: pick(xAxis, AXIS_META_CONFIG_KEYS), + [yField]: pick(yAxis, AXIS_META_CONFIG_KEYS), + }); + + chart.scale(scales); + return params; } @@ -43,7 +65,12 @@ function meta(params: Params): Params { * @param params */ function axis(params: Params): Params { - // TODO + const { chart, options } = params; + const { xAxis, yAxis, xField, yField } = options; + + chart.axis(xField, xAxis); + chart.axis(yField, yAxis); + return params; } @@ -52,7 +79,59 @@ function axis(params: Params): Params { * @param params */ function legend(params: Params): Params { - // TODO + const { chart, options } = params; + const { legend, seriesField } = options; + + if (legend && seriesField) { + chart.legend(seriesField, legend); + } + + return params; +} + +/** + * 样式 + * @param params + */ +function style(params: Params): Params { + const { chart, options } = params; + const { xField, yField, pointStyle, colorField } = options; + + const geometry = chart.geometries[0]; + + if (pointStyle && geometry) { + if (isFunction(pointStyle)) { + geometry.style(`${xField}*${yField}*${colorField}`, pointStyle); + } else { + geometry.style(pointStyle); + } + } + + return params; +} + +/** + * 数据标签 + * @param params + */ +function label(params: Params): Params { + const { chart, options } = params; + const { label, yField } = options; + + const scatterGeometry = findGeometry(chart, 'point'); + + // label 为 false, 空 则不显示 label + if (!label) { + scatterGeometry.label(false); + } else { + const { callback, ...cfg } = label; + scatterGeometry.label({ + fields: [yField], + callback, + cfg, + }); + } + return params; } @@ -63,5 +142,5 @@ function legend(params: Params): Params { */ export function adaptor(params: Params) { // flow 的方式处理所有的配置到 G2 API - flow(field, meta, axis, legend, tooltip)(params); + flow(field, meta, axis, legend, tooltip, style, label)(params); } diff --git a/src/plots/scatter/reflect.ts b/src/plots/scatter/reflect.ts new file mode 100644 index 0000000000..b44135f5cb --- /dev/null +++ b/src/plots/scatter/reflect.ts @@ -0,0 +1,25 @@ +import { isArray, isFunction } from '@antv/util'; + +export const REFLECTS = { + shape: { + /** 对应 G2 动作 */ + action: 'shape', + /** 映射字段,默认使用 xField */ + field: 'shapeField', + /** + * 条件组件 满足其一则使用 fn(field, value[] | callback) + * eg: geometry.size('field', [10, 20]) OR geometry.size(10) + */ + rules: [isArray, isFunction], + }, + size: { + action: 'size', + field: 'sizeField', + rules: [isArray, isFunction], + }, + color: { + action: 'color', + field: 'colorField', + rules: [isArray, isFunction], + }, +}; diff --git a/src/plots/scatter/types.ts b/src/plots/scatter/types.ts index 37ae7eb943..f4e864cb07 100644 --- a/src/plots/scatter/types.ts +++ b/src/plots/scatter/types.ts @@ -1,18 +1,80 @@ import { Options } from '../../types'; +import { ShapeStyle } from '../../types/style'; +import { Label } from '../../types/label'; + +interface PointStyle { + /** 填充色 会覆盖 color 配置 */ + readonly fill?: string; + /** 描边颜色 */ + readonly stroke?: string; + /** 线宽 */ + readonly lineWidth?: number; + /** 虚线显示 */ + readonly lineDash?: number[]; + /** 透明度 */ + readonly opacity?: number; + /** 填充透明度 */ + readonly fillOpacity?: number; + /** 描边透明度 */ + readonly strokeOpacity?: number; +} + +interface Quadrant { + /** 是否显示 */ + readonly visible?: boolean; + /** x 方向上的象限分割基准线,默认为 0 */ + readonly xBaseline?: number; + /** y 方向上的象限分割基准线,默认为 0 */ + readonly yBaseline?: number; + /** 配置象限分割线的样式 */ + readonly lineStyle?: ShapeStyle; + /** 象限样式 */ + readonly regionStyle?: RegionStyle[]; + /** 象限文本配置 */ + readonly label?: Label; +} + +interface RegionStyle { + /** 填充色 */ + readonly fill?: string; + /** 不透明度 */ + readonly opacity?: number; +} + +interface TrendLine { + /** 是否显示 */ + readonly visible?: boolean; + /** 趋势线类型 */ + readonly type?: string; + /** 配置趋势线样式 */ + readonly style?: ShapeStyle; + /** 是否绘制置信区间曲线 */ + readonly showConfidence?: boolean; + /** 配置置信区间样式 */ + readonly confidenceStyle?: ShapeStyle; +} export interface ScatterOptions extends Options { /** x 轴字段 */ readonly xField: string; - /** y 轴字段 */ readonly yField: string; - /** 分组字段 */ readonly seriesField?: string; - + /** 点大小映射对应的数据字段名 */ + readonly sizeField?: string; /** 散点图大小 */ - readonly symbolSize?: number | [number, number] | ((value: number) => number); - + readonly size?: number | [number, number] | ((value: number) => number); + /** 点形状映射对应的数据字段名 */ + readonly shapeField?: string; /** 散点图形状 */ - readonly shape?: string[] | ((item: any[]) => string | string[]); + readonly shape?: string | string[] | ((shape: string) => string); + /** 散点图样式 */ + readonly pointStyle?: PointStyle | ((x: number, y: number, colorfield?: string) => ShapeStyle); + /** 点颜色映射对应的数据字段名 */ + readonly colorField?: string; + /** 四象限组件 */ + readonly quadrant?: Quadrant; + /** 趋势线组件,为图表添加回归曲线 */ + readonly trendLine?: TrendLine; }