diff --git a/__tests__/unit/core/index-spec.ts b/__tests__/unit/core/index-spec.ts index b26f4c003f..d82b638a6c 100644 --- a/__tests__/unit/core/index-spec.ts +++ b/__tests__/unit/core/index-spec.ts @@ -1,5 +1,6 @@ -import { Line, G2 } from '../../../src'; +import { Line, G2, Pie } from '../../../src'; import { partySupport } from '../../data/party-support'; +import { salesByArea } from '../../data/sales'; import { createDiv } from '../../utils/dom'; G2.registerTheme('new-theme', { @@ -115,4 +116,32 @@ describe('core', () => { _data: 1, }); }); + + it('state', async () => { + const pie = new Pie(createDiv('饼图状态'), { + width: 400, + height: 400, + data: salesByArea, + angleField: 'sales', + colorField: 'area', + radius: 0.8, + autoFit: false, + interactions: [{ name: 'element-selected' }], + }); + + pie.render(); + + // 注意,如果 autoFit 会触发一次 render,导致 setState 的状态又还原了(实际场景,自己处理一个时机即可) + pie.setState('selected', (data) => (data as any).area === salesByArea[0].area); + expect(pie.getStates().length).toBe(1); + + pie.chart.geometries[0].elements[0].setState('selected', false); + expect(pie.getStates().length).toBe(0); + + pie.setState('selected', (data) => (data as any).area === salesByArea[2].area); + expect(pie.getStates().length).toBe(1); + // 取消 selected + pie.setState('selected', (data) => (data as any).area === salesByArea[2].area, false); + expect(pie.getStates().length).toBe(0); + }); }); diff --git a/__tests__/unit/plots/pie/state-spec.ts b/__tests__/unit/plots/pie/state-spec.ts new file mode 100644 index 0000000000..9f42b4f749 --- /dev/null +++ b/__tests__/unit/plots/pie/state-spec.ts @@ -0,0 +1,82 @@ +import { Pie } from '../../../../src'; +import { POSITIVE_NEGATIVE_DATA } from '../../../data/common'; +import { createDiv } from '../../../utils/dom'; + +describe('pie', () => { + const data = POSITIVE_NEGATIVE_DATA.filter((o) => o.value > 0).map((d, idx) => + idx === 1 ? { ...d, type: 'item1' } : d + ); + + const options = { + width: 400, + height: 300, + data, + angleField: 'value', + colorField: 'type', + color: ['blue', 'red', 'yellow', 'lightgreen', 'lightblue', 'pink'], + radius: 0.8, + autoFit: false, + }; + + it('set statesStyle', () => { + const pie = new Pie(createDiv(), { + ...options, + state: { + selected: { + style: { + lineWidth: 4, + fill: 'red', + }, + }, + inactive: { + style: { + fill: 'blue', + }, + }, + }, + }); + + pie.render(); + + pie.setState('selected', (d: any) => d.type === data[0].type); + const shape = pie.chart.geometries[0].elements[0].shape; + + expect(shape.attr('lineWidth')).toBe(4); + expect(shape.attr('fill')).toBe('red'); + + // // 取消 selected + pie.setState('selected', (d: any) => d.type === data[0].type, false); + pie.setState('inactive', (d: any) => d.type === data[0].type); + expect(shape.attr('fill')).toBe('blue'); + expect(pie.getStates()[0].state).toBe('inactive'); + }); + + it('set statesStyle by theme', () => { + const pie = new Pie(createDiv(), { + ...options, + theme: { + geometries: { + interval: { + rect: { + active: { + style: { + fill: 'yellow', + fillOpacity: 0.65, + }, + }, + }, + }, + }, + }, + }); + + pie.render(); + + pie.setState('active', (d: any) => d.type === data[2].type); + const shape = pie.chart.geometries[0].elements[2].shape; + + expect(shape.attr('fill')).toBe('yellow'); + expect(shape.attr('fillOpacity')).toBe(0.65); + expect(pie.getStates()[0].state).toBe('active'); + }); +}); diff --git a/examples/pie/basic/demo/meta.json b/examples/pie/basic/demo/meta.json index 416edb2d81..fc1cff026d 100644 --- a/examples/pie/basic/demo/meta.json +++ b/examples/pie/basic/demo/meta.json @@ -21,12 +21,12 @@ "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*ZztJQa4RLwoAAAAAAAAAAABkARQnAQ" }, { - "filename": "spider-label.ts", + "filename": "pie-state.ts", "title": { - "zh": "饼图-图形标签蜘蛛布局", - "en": "Pie chart - spider-layout label" + "zh": "饼图-设置条件状态", + "en": "Pie chart - set condition state" }, - "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*ahB1Qp7T-C8AAAAAAAAAAABkARQnAQ" + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*y8zjS5DZib8AAAAAAAAAAAAAARQnAQ" }, { "filename": "pie-texture.ts", diff --git a/examples/pie/basic/demo/pie-state.ts b/examples/pie/basic/demo/pie-state.ts new file mode 100644 index 0000000000..2c9bebfb38 --- /dev/null +++ b/examples/pie/basic/demo/pie-state.ts @@ -0,0 +1,43 @@ +import { Pie } from '@antv/g2plot'; + +const data = [ + { type: '分类一', value: 27 }, + { type: '分类二', value: 25 }, + { type: '分类三', value: 18 }, + { type: '分类四', value: 15 }, + { type: '分类五', value: 10 }, + { type: '其他', value: 5 }, +]; + +const piePlot = new Pie('container', { + appendPadding: 10, + data, + angleField: 'value', + colorField: 'type', + radius: 0.8, + label: { + type: 'inner', + content: '{name} {percentage}', + style: { + fill: '#fff', + fontSize: 14, + }, + }, + // 设置 状态样式 + state: { + active: { + style: { + lineWidth: 0, + fillOpacity: 0.65, + }, + }, + }, + // 添加 element 选中和激活交互 + interactions: [{ name: 'element-selected' }, { name: 'element-active' }], +}); + +piePlot.render(); + +// 初始化设置默认状态;状态可叠加,可通过回调设置 +piePlot.setState('active', (data) => (data as any).type === '分类一'); +piePlot.setState('selected', (d) => (data as any).type === '分类一' || (data as any).type === '分类二'); diff --git a/src/adaptor/common.ts b/src/adaptor/common.ts index 46132d1b6a..b1d2ac889b 100644 --- a/src/adaptor/common.ts +++ b/src/adaptor/common.ts @@ -84,3 +84,20 @@ export function theme>(params: Params): Para } return params; } + +/** + * 状态 state 配置 + * @param params + */ +export function state(params: Params): Params { + const { chart, options } = params; + const { state } = options; + + if (state) { + each(chart.geometries, (geometry: Geometry) => { + geometry.state(state); + }); + } + + return params; +} diff --git a/src/core/plot.ts b/src/core/plot.ts index 0be5601129..0731e888b4 100644 --- a/src/core/plot.ts +++ b/src/core/plot.ts @@ -1,10 +1,11 @@ import { Chart, Event } from '@antv/g2'; -import { deepMix } from '@antv/util'; +import Element from '@antv/g2/lib/geometry/element'; +import { deepMix, each } from '@antv/util'; import EE from '@antv/event-emitter'; import { bind } from 'size-sensor'; import { Adaptor } from './adaptor'; -import { Options, Data, Size } from '../types'; -import { getContainerSize } from '../utils'; +import { Options, Data, StateName, StateCondition, Size, StateObject } from '../types'; +import { getContainerSize, getAllElements } from '../utils'; /** 单独 pick 出来的用于基类的类型定义 */ type PickOptions = Pick< @@ -151,6 +152,40 @@ export abstract class Plot extends EE { this.render(); } + /** + * 设置状态 + * @param type 状态类型,支持 'active' | 'inactive' | 'selected' 三种 + * @param conditions 条件,支持数组 + * @param status 是否激活,默认 true + */ + public setState(type: StateName, condition: StateCondition, status: boolean = true) { + const elements = getAllElements(this.chart); + + each(elements, (ele: Element) => { + if (condition(ele.getData())) { + ele.setState(type, status); + } + }); + } + + /** + * 获取状态 + */ + public getStates(): StateObject[] { + const elements = getAllElements(this.chart); + + const stateObjects: StateObject[] = []; + each(elements, (element: Element) => { + const data = element.getData(); + const states = element.getStates(); + each(states, (state) => { + stateObjects.push({ data, state, geometry: element.geometry, element }); + }); + }); + + return stateObjects; + } + /** * 更新数据 * @param options diff --git a/src/plots/pie/adaptor.ts b/src/plots/pie/adaptor.ts index b6eeed76d1..a9b3e2249d 100644 --- a/src/plots/pie/adaptor.ts +++ b/src/plots/pie/adaptor.ts @@ -1,6 +1,6 @@ import { deepMix, each, every, filter, get, isFunction, isString, isNil } from '@antv/util'; import { Params } from '../../core/adaptor'; -import { legend, tooltip, interaction, animation, theme } from '../../adaptor/common'; +import { legend, tooltip, interaction, animation, theme, state } from '../../adaptor/common'; import { Data } from '../../types'; import { flow, LEVEL, log, template } from '../../utils'; import { PieOptions } from './types'; @@ -161,7 +161,8 @@ function style(params: Params): Params { /** * annotation 配置 - * 1. 中心文本 + * 内置标注: + * 1. 中心文本 * @param params */ function annotation(params: Params): Params { @@ -188,7 +189,10 @@ function annotation(params: Params): Params { content: '', }; - const contentFormatter = get(content, 'formatter'); + const getStatisticData = (data: Data) => ({ + title: '总计', + value: getTotalValue(data, angleField), + }); if (title !== false) { let titleLineHeight = get(title, 'style.lineHeight'); @@ -201,10 +205,7 @@ function annotation(params: Params): Params { type: 'text', position: ['50%', '50%'], content: (filterData: Data) => { - const statisticData = { - title: '总计', - value: getTotalValue(filterData, angleField), - }; + const statisticData = getStatisticData(filterData); return titleFormatter ? titleFormatter(statisticData, filterData) : statisticData.title; }, ...deepMix( @@ -213,6 +214,9 @@ function annotation(params: Params): Params { offsetY: content === false ? 0 : -titleLineHeight, // append-info key: 'statistic', + style: { + textAlign: 'center', + }, }, title ), @@ -224,14 +228,13 @@ function annotation(params: Params): Params { if (!valueLineHeight) { valueLineHeight = get(content, 'style.fontSize', 20); } + const contentFormatter = get(content, 'formatter'); + statisticContent = { type: 'text', position: ['50%', '50%'], content: (filterData: Data) => { - const statisticData = { - title: '总计', - value: getTotalValue(filterData, angleField), - }; + const statisticData = getStatisticData(filterData); return contentFormatter ? contentFormatter(statisticData, filterData) : statisticData.value; }, ...deepMix( @@ -241,6 +244,9 @@ function annotation(params: Params): Params { offsetY: title === false ? 0 : valueLineHeight, // append-info key: 'statistic', + style: { + textAlign: 'center', + }, }, content ), @@ -266,5 +272,5 @@ function annotation(params: Params): Params { */ export function adaptor(params: Params) { // flow 的方式处理所有的配置到 G2 API - flow(field, meta, theme, coord, legend, tooltip, label, style, annotation, interaction, animation)(params); + flow(field, meta, theme, coord, legend, tooltip, label, state, style, annotation, interaction, animation)(params); } diff --git a/src/plots/pie/types.ts b/src/plots/pie/types.ts index 266f50d69d..19d29c693f 100644 --- a/src/plots/pie/types.ts +++ b/src/plots/pie/types.ts @@ -12,22 +12,25 @@ export type StatisticData = { */ type Statistic = Readonly<{ /** 自定义 title 标签 */ - title?: { - formatter?: (item: StatisticData, data: LooseObject | LooseObject[]) => string; - rotate?: number; - offsetX?: number; - offsetY?: number; - style?: ShapeStyle; - }; + title?: + | boolean + | { + formatter?: (item: StatisticData, data: LooseObject | LooseObject[]) => string; + rotate?: number; + offsetX?: number; + offsetY?: number; + style?: ShapeStyle; + }; /** 自定义 content 内容 */ - content?: { - formatter?: (item: StatisticData, data: LooseObject | LooseObject[]) => string; - rotate?: number; - offsetX?: number; - offsetY?: number; - style?: ShapeStyle; - }; - // todo 提供 htmlContent 的方式,自由定制中心文本 + content?: + | boolean + | { + formatter?: (item: StatisticData, data: LooseObject | LooseObject[]) => string; + rotate?: number; + offsetX?: number; + offsetY?: number; + style?: ShapeStyle; + }; }>; export interface PieOptions extends Options { diff --git a/src/types/common.ts b/src/types/common.ts index 7b0794515c..3ef46ca20d 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -4,6 +4,7 @@ import { Tooltip } from './tooltip'; import { Legend } from './legend'; import { Interaction } from './interaction'; import { Animation } from './animation'; +import { State } from './state'; /** 一条数据记录 */ export type Datum = Record; @@ -103,4 +104,6 @@ export type Options = { readonly legend?: Legend; readonly animation?: Animation; readonly interactions?: Interaction[]; + // 配置 active,inactive,selected 三种状态的样式,也可在 Theme 主题中配置 + readonly state?: State; }; diff --git a/src/types/index.ts b/src/types/index.ts index f63dfab5c6..f7aae7e7eb 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,4 @@ export * from './common'; export * from './tooltip'; export * from './tuple'; +export * from './state'; diff --git a/src/types/state.ts b/src/types/state.ts new file mode 100644 index 0000000000..0215f60528 --- /dev/null +++ b/src/types/state.ts @@ -0,0 +1,14 @@ +import { StateOption, Datum, Data } from '@antv/g2/lib/interface'; +import Element from '@antv/g2/lib/geometry/element'; +import { Geometry } from '@antv/g2'; + +export type State = StateOption; + +/** 状态名称,G2 Element 开放 'active' | 'inactive' | 'selected' 三种状态 */ +export type StateName = 'active' | 'inactive' | 'selected'; + +/** 状态条件 */ +export type StateCondition = (data: Datum | Data) => boolean; + +/** 状态对象, 可通过 `plot.getStates()` 获取 */ +export type StateObject = { data: Datum | Data; state: string; geometry: Geometry; element: Element }; diff --git a/src/utils/geometry.ts b/src/utils/geometry.ts index 2c6a77c277..90864fbb54 100644 --- a/src/utils/geometry.ts +++ b/src/utils/geometry.ts @@ -1,4 +1,6 @@ -import { Geometry, View } from '@antv/g2'; +import { Chart, Geometry, View } from '@antv/g2'; +import Element from '@antv/g2/lib/geometry/element'; +import { reduce } from '@antv/util'; /** * 在 Chart 中查找第一个指定 type 类型的 geometry @@ -8,3 +10,16 @@ import { Geometry, View } from '@antv/g2'; export function findGeometry(chart: View, type: string): Geometry { return chart.geometries.find((g: Geometry) => g.type === type); } + +/** + * 获取 Chart 的 所有 elements + */ +export function getAllElements(chart: Chart): Element[] { + return reduce( + chart.geometries, + (r: Element[], geometry: Geometry) => { + return r.concat(geometry.elements); + }, + [] + ); +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 77a0873e12..44a797a2af 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,4 +3,4 @@ export { pick } from './pick'; export { template } from './template'; export { log, invariant, LEVEL } from './invariant'; export { getContainerSize } from './dom'; -export { findGeometry } from './geometry'; +export { findGeometry, getAllElements } from './geometry';