Skip to content

Commit

Permalink
feat(legend): 添加图例的正反选功能 (#3756)
Browse files Browse the repository at this point in the history
* feat(legend): 添加图例的正反选功能(close: #3736)

* feat(legend): 把 radio 变成 focus 功能
  • Loading branch information
pearmini committed Dec 29, 2021
1 parent c01d971 commit 2c92f5d
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 13 deletions.
33 changes: 23 additions & 10 deletions docs/api/general/legend.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,11 @@ chart.legend('type', {

适用于 <tag color="green" text="分类图例">分类图例</tag>,图例项 name 文本的配置。_LegendItemNameCfg_ 配置如下:

| 参数名 | 类型 | 是否必选 | 默认值 | 描述 |
| --------- | ------------------------------------------------------- | -------- | ------ | -------------------------------- |
| style | _((item: ListItem, index: number, items: ListItem[]) => ShapeAttrs) \| ShapeAttrs_ | | - | 文本样式配置项 |
| spacing | number | | - | 图例项 marker 同后面 name 的间距 |
| formatter | `(text: string, item: ListItem, index: number) => any;` | | | 格式化函数 |
| 参数名 | 类型 | 是否必选 | 默认值 | 描述 |
| --------- | ---------------------------------------------------------------------------------- | -------- | ------ | -------------------------------- |
| style | _((item: ListItem, index: number, items: ListItem[]) => ShapeAttrs) \| ShapeAttrs_ | | - | 文本样式配置项 |
| spacing | number | | - | 图例项 marker 同后面 name 的间距 |
| formatter | `(text: string, item: ListItem, index: number) => any;` | | | 格式化函数 |

其中, `ShapeAttrs` 详细配置见:[文档](/zh/docs/api/shape/shape-attrs)`ListItem` 配置如下:

Expand Down Expand Up @@ -209,11 +209,11 @@ type Marker = {

适用于 <tag color="green" text="分类图例">分类图例</tag>,图例项 value 附加值的配置项。_LegendItemValueCfg_ 配置如下:

| 参数名 | 类型 | 是否必选 | 默认值 | 描述 |
| ---------- | ------------------------------------------------------- | -------- | ------- | -------------------------------------------------- |
| alignRight | boolean | | `false` | 是否右对齐,默认为 false,仅当设置图例项宽度时生效 |
| style | _((item: ListItem, index: number, items: ListItem[]) => ShapeAttrs) \| ShapeAttrs_ | | - | 文本样式配置项 |
| formatter | `(text: string, item: ListItem, index: number) => any;` | | | 格式化函数 |
| 参数名 | 类型 | 是否必选 | 默认值 | 描述 |
| ---------- | ---------------------------------------------------------------------------------- | -------- | ------- | -------------------------------------------------- |
| alignRight | boolean | | `false` | 是否右对齐,默认为 false,仅当设置图例项宽度时生效 |
| style | _((item: ListItem, index: number, items: ListItem[]) => ShapeAttrs) \| ShapeAttrs_ | | - | 文本样式配置项 |
| formatter | `(text: string, item: ListItem, index: number) => any;` | | | 格式化函数 |

其中, `ShapeAttrs` 详细配置见:[文档](/zh/docs/api/shape/shape-attrs)`ListItem` 配置如下:

Expand All @@ -240,6 +240,19 @@ type Marker = {
};
```

### legendOption.radio

<description> _LegendRadio_ **optional** </description>

适用于 <tag color="green" text="分类图例">分类图例</tag>,当 radio 为 truthy 的时候开启正反选功能:鼠标移动到图例上面的时候会出现 radio 按钮,点击按钮的时候,如果当前图例没有被选中,那么只选中该图例,并且展示对应数据,否者恢复默认状态。

_LegendRadio_ 配置如下:

| 参数名 | 类型 | 是否必选 | 默认值 | 描述 |
| ---------- | ---------------------------------------------------------------------------------- | -------- | ------- | -------------------------------------------------- |
| style | _ShapeAttrs_ | | - | 文本样式配置项 |

### legendOption.animate

<description> _boolean_ **optional** _default:_ `true` </description>
Expand Down
23 changes: 20 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ import ListHighlight from './interaction/action/component/list-highlight';
import ListSelected from './interaction/action/component/list-selected';
import ListUnchecked from './interaction/action/component/list-unchecked';
import ListChecked from './interaction/action/component/list-checked';
import ListFocus from './interaction/action/component/list-focus';
import ListRadio from './interaction/action/component/list-radio';

import CircleMask from './interaction/action/mask/circle';
import DimMask from './interaction/action/mask/dim-rect';
Expand Down Expand Up @@ -248,6 +250,8 @@ registerAction('list-selected', ListSelected);
registerAction('list-highlight', ListHighlight);
registerAction('list-unchecked', ListUnchecked);
registerAction('list-checked', ListChecked);
registerAction('list-focus', ListFocus);
registerAction('list-radio', ListRadio);

registerAction('legend-item-highlight', ListHighlight, {
componentNames: ['legend'],
Expand Down Expand Up @@ -560,10 +564,23 @@ registerInteraction('element-single-selected', {
// 筛选数据
registerInteraction('legend-filter', {
showEnable: [
{ trigger: 'legend-item:mouseenter', action: 'cursor:pointer' },
{ trigger: 'legend-item:mouseleave', action: 'cursor:default' },
{ trigger: 'legend-item:mouseenter', action: ['cursor:pointer', 'list-radio:show'] },
{ trigger: 'legend-item:mouseleave', action: ['cursor:default', 'list-radio:hide'] },
],
start: [
{
trigger: 'legend-item:click',
isEnable: (context) => {
return !context.isInShape('legend-item-radio');
},
action: ['list-unchecked:toggle', 'data-filter:filter', 'list-radio:show'],
},
// 正反选数据: 只有当 radio === truthy 的时候才会有 legend-item-radio 这个元素
{
trigger: 'legend-item-radio:click',
action: ['list-focus:toggle', 'data-filter:filter', 'list-radio:show'],
},
],
start: [{ trigger: 'legend-item:click', action: ['list-unchecked:toggle', 'data-filter:filter'] }],
});

// 筛选数据
Expand Down
24 changes: 24 additions & 0 deletions src/interaction/action/component/list-focus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import ListState from './list-state';

const STATUS_UNCHECKED = 'unchecked';

class ListFocus extends ListState {
public toggle() {
const triggerInfo = this.getTriggerListInfo();
if (triggerInfo?.item) {
const { list, item: clickedItem } = triggerInfo;
const items = list.getItems();
const checkedItems = items.filter((t) => !list.hasState(t, STATUS_UNCHECKED));
for (const item of items) {
if (item === clickedItem) {
list.setItemState(item, STATUS_UNCHECKED, false);
} else {
const status = checkedItems.length > 1;
list.setItemState(item, STATUS_UNCHECKED, status);
}
}
}
}
}

export default ListFocus;
23 changes: 23 additions & 0 deletions src/interaction/action/component/list-radio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import ListState from './list-state';

const STATUS_SHOW = 'showRadio';

class ListRadio extends ListState {
public show() {
const triggerInfo = this.getTriggerListInfo();
if (triggerInfo?.item) {
const { list, item } = triggerInfo;
list.setItemState(item, STATUS_SHOW, true);
}
}

public hide() {
const triggerInfo = this.getTriggerListInfo();
if (triggerInfo?.item) {
const { list, item } = triggerInfo;
list.setItemState(item, STATUS_SHOW, false);
}
}
}

export default ListRadio;
5 changes: 5 additions & 0 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { View } from './chart';
import { Facet } from './facet';
import Element from './geometry/element';
import { PaddingCalCtor } from './chart/layout/padding-cal';
import { LegendRadio } from '@antv/component';

// ============================ 基础类型 ============================
/** 通用对象 */
Expand Down Expand Up @@ -1063,6 +1064,10 @@ export interface LegendCfg extends Omit<CategoryLegendCfg, 'marker'> {
* **分类图例适用**,用户自己配置图例项的内容。
*/
items?: LegendItem[];
/**
* **分类图里适用**,用来配置正反选功能
*/
radio?:LegendRadio
/**
* **分类图例适用**,是否将图例项逆序展示。
*/
Expand Down
131 changes: 131 additions & 0 deletions tests/unit/component/legend-category-radio-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { Chart } from '../../../src/';
import { createDiv, removeDom } from '../../util/dom';
import { COMPONENT_TYPE } from '../../../src/constant';
import { GroupComponent, GroupComponentCfg } from '../../../src/dependents';

function renderLegend(options = {}) {
const CITY_SALE = [
{ city: '杭州', sale: 100 },
{ city: '广州', sale: 30 },
{ city: '上海', sale: 110 },
{ city: '呼和浩特', sale: 40 },
];
const div = createDiv();
const chart = new Chart({
container: div,
width: 400,
height: 300,
autoFit: false,
});

chart.data(CITY_SALE);
chart.interval().position('city*scale').color('city');
chart.legend(options);
chart.render();

const [legend] = chart.getComponents().filter((co) => co.type === COMPONENT_TYPE.LEGEND);
return {
chart,
legend: legend.component as GroupComponent<GroupComponentCfg>,
};
}

describe('Legend category radio', () => {
test('默认 category legend 没有 radio', () => {
const { legend } = renderLegend();
expect(legend.getElementsByName('legend-item-radio').length).toBe(0);
});

test('选项 radio:{} 可以开启 radio', () => {
const { legend } = renderLegend({ radio: {} });
const radios = legend.getElementsByName('legend-item-radio');
const [radio] = radios;
expect(radios.length).toBe(4);
expect(radio.attr('opacity')).toBe(0);
expect(radio.attr('stroke')).toBe('#000000');
expect(radio.attr('fill')).toBe('#ffffff');
});

test('选项 radio:styles 可以配置 radio 样式', () => {
const { legend } = renderLegend({
radio: {
stroke: 'red',
fill: 'blue',
},
});
const [radio] = legend.getElementsByName('legend-item-radio');
expect(radio.attr('opacity')).toBe(0);
expect(radio.attr('stroke')).toBe('red');
expect(radio.attr('fill')).toBe('blue');
});

test('默认 radio 的 opacity 是 0,鼠标移动上去 opacity 是 0.45,移出是 0', () => {
const { legend, chart } = renderLegend({ radio: {} });
const radios = legend.getElementsByName('legend-item-radio');
const [radio] = radios;

expect(radio.attr('opacity')).toBe(0);

const target = legend.get('container').findById('-legend-item-杭州-name');

chart.emit('legend-item:mouseenter', { x: 50, y: 330, target });
expect(radio.attr('opacity')).toBe(0.45);

chart.emit('legend-item:mouseleave', { x: 50, y: 330, target });
expect(radio.attr('opacity')).toBe(0);
});

test('点击 radio,如果有多个 item 被选中,那么只选中对应 item,否者恢复默认状态', () => {
const { legend, chart } = renderLegend({ radio: {} });
const radios = legend.getElementsByName('legend-item-radio');
const [radio] = radios;
const getItems = (legend) =>
legend.get('items').map((item) => ({
value: item.value,
unchecked: item.unchecked,
}));

expect(radio.attr('opacity')).toBe(0);

const target = legend.get('container').findById('-legend-item-杭州-name');

// 点击 legend-item 不生效
chart.emit('legend-item:click', { x: 50, y: 330, target });
expect(radio.attr('opacity')).toBe(0.45);
expect(getItems(legend)).toEqual([
{ value: '杭州', unchecked: true },
{ value: '广州', unchecked: false },
{ value: '上海', unchecked: false },
{ value: '呼和浩特', unchecked: false },
]);

chart.emit('legend-item-radio:click', { x: 50, y: 330, target });
expect(radio.attr('opacity')).toBe(0.45);
expect(getItems(legend)).toEqual([
{ value: '杭州', unchecked: false },
{ value: '广州', unchecked: true },
{ value: '上海', unchecked: true },
{ value: '呼和浩特', unchecked: true },
]);

chart.emit('legend-item-radio:click', { x: 50, y: 330, target });
expect(radio.attr('opacity')).toBe(0.45);
expect(getItems(legend)).toEqual([
{ value: '杭州', unchecked: false },
{ value: '广州', unchecked: false },
{ value: '上海', unchecked: false },
{ value: '呼和浩特', unchecked: false },
]);

const target2 = legend.get('container').findById('-legend-item-广州-name');
chart.emit('legend-item-radio:click', { x: 50, y: 330, target });
chart.emit('legend-item:click', { x: 50, y: 330, target: target2 });
chart.emit('legend-item-radio:click', { x: 50, y: 330, target });
expect(getItems(legend)).toEqual([
{ value: '杭州', unchecked: false },
{ value: '广州', unchecked: true },
{ value: '上海', unchecked: true },
{ value: '呼和浩特', unchecked: true },
]);
});
});

0 comments on commit 2c92f5d

Please sign in to comment.