Skip to content

Commit

Permalink
feat(legend): 添加正反选按钮 (#259)
Browse files Browse the repository at this point in the history
* feat(legend): 添加正反选按钮

* chore: update version
  • Loading branch information
pearmini authored Dec 29, 2021
1 parent 5f0a66d commit 2e8c756
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 18 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@antv/component",
"version": "0.8.20",
"version": "0.8.21",
"description": "The component module for antv",
"author": "https://github.com/orgs/antvis/people",
"license": "MIT",
Expand Down
75 changes: 65 additions & 10 deletions src/legend/category.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { IGroup } from '@antv/g-base';
import { clamp, deepMix, each, filter, get, mix, isNumber, isFunction } from '@antv/util';
import { IList } from '../interfaces';
import { CategoryLegendCfg, LegendPageNavigatorCfg, LegendItemNameCfg, LegendMarkerCfg, ListItem } from '../types';
import {
CategoryLegendCfg,
LegendPageNavigatorCfg,
LegendItemNameCfg,
LegendMarkerCfg,
ListItem,
LegendRadio,
} from '../types';
import { ellipsisLabel } from '../util/label';
import { getMatrixByAngle, getMatrixByTranslate } from '../util/matrix';
import { getStatesStyle } from '../util/state';
Expand Down Expand Up @@ -36,7 +43,7 @@ const textStyle = {
textAlign: 'start',
textBaseline: 'middle',
fontFamily: Theme.fontFamily,
fontWeight: "normal",
fontWeight: 'normal',
lineHeight: 12,
};

Expand Down Expand Up @@ -64,6 +71,7 @@ class Category extends LegendBase<CategoryLegendCfg> implements IList {
maxWidth: null,
maxHeight: null,
marker: {},
radio: null,
items: [],
itemStates: {},
itemBackground: {},
Expand Down Expand Up @@ -92,7 +100,7 @@ class Category extends LegendBase<CategoryLegendCfg> implements IList {
},
pageNavigator: DEFAULT_PAGE_NAVIGATOR,
itemName: {
spacing: 16, // 如果右边有 value 使用这个间距
spacing: 16, // 如果右边有 value 或者 RadioIcon 使用这个间距
style: textStyle,
},
marker: {
Expand All @@ -106,6 +114,7 @@ class Category extends LegendBase<CategoryLegendCfg> implements IList {
alignRight: false, // 只有itemWidth 不为 null 时此属性有效
formatter: null,
style: textStyle,
spacing: 6, // 如果右边有 RadioIcon 使用在这个间距
},
itemStates: {
active: {
Expand Down Expand Up @@ -334,7 +343,7 @@ class Category extends LegendBase<CategoryLegendCfg> implements IList {
if (itemHeight < fontSize) {
itemHeight = fontSize;
}
})
});
} else if (style) {
itemHeight = style.fontSize;
}
Expand Down Expand Up @@ -390,6 +399,7 @@ class Category extends LegendBase<CategoryLegendCfg> implements IList {
...textStyle,
...(isFunction(style) ? style(item, index, this.getItems()) : style),
};

return this.addShape(container, {
type: 'text',
id: this.getElementId(`item-${item.id}-${textName}`),
Expand All @@ -398,6 +408,43 @@ class Category extends LegendBase<CategoryLegendCfg> implements IList {
});
}

private drawRadio(container: IGroup, style: LegendRadio, item: ListItem, itemHeight: number, x: number) {
const r = itemHeight / 2;
const lineWidth = (r * 3.6) / 8;
const [x0, y0] = [x + r, itemHeight / 2 - r];
const [x1, y1] = [x0 + r, y0 + r];
const [x2, y2] = [x0, y1 + r];
const [x3, y3] = [x, y0 + r];
const { showRadio } = item;
const attrs = {
path: [
['M', x0, y0],
['A', r, r, 0, 0, 1, x1, y1],
['L', x1 - lineWidth, y1],
['L', x1, y1],
['A', r, r, 0, 0, 1, x2, y2],
['L', x2, y2 - lineWidth],
['L', x2, y2],
['A', r, r, 0, 0, 1, x3, y3],
['L', x3 + lineWidth, y3],
['L', x3, y3],
['A', r, r, 0, 0, 1, x0, y0],
['L', x0, y0 + lineWidth],
],
stroke: '#000000',
fill: '#ffffff',
...style,
opacity: showRadio ? 0.45 : 0,
};

return this.addShape(container, {
type: 'path',
id: this.getElementId(`item-${item.id}-radio`),
name: 'legend-item-radio',
attrs,
});
}

// 绘制图例项
private drawItem(item: ListItem, index: number, itemHeight: number, itemGroup: IGroup) {
const groupId = `item-${item.id}`;
Expand All @@ -422,6 +469,7 @@ class Category extends LegendBase<CategoryLegendCfg> implements IList {
const itemName = this.get('itemName');
const itemValue = this.get('itemValue');
const itemBackground = this.get('itemBackground');
const radio = this.get('radio');
const itemWidth = this.getLimitItemWidth();

let curX = 0; // 记录当前 x 的位置
Expand All @@ -446,6 +494,7 @@ class Category extends LegendBase<CategoryLegendCfg> implements IList {
}
curX = nameShape.getBBox().maxX + itemName.spacing;
}

if (itemValue) {
const valueShape = this.drawItemText(subGroup, 'value', itemValue, item, itemHeight, curX, index);
if (itemWidth) {
Expand All @@ -459,7 +508,13 @@ class Category extends LegendBase<CategoryLegendCfg> implements IList {
ellipsisLabel(true, valueShape, clamp(itemWidth - curX, 0, itemWidth));
}
}
curX = valueShape.getBBox().maxX + itemValue.spacing;
}

if (radio) {
this.drawRadio(subGroup, radio, item, itemHeight, curX);
}

// 添加透明的背景,便于拾取和包围盒计算
if (itemBackground) {
const bbox = subGroup.getBBox();
Expand Down Expand Up @@ -722,13 +777,13 @@ class Category extends LegendBase<CategoryLegendCfg> implements IList {
const translate =
layout === 'horizontal'
? {
x: 0,
y: pageHeight * (1 - currentPageIndex),
}
x: 0,
y: pageHeight * (1 - currentPageIndex),
}
: {
x: pageWidth * (1 - currentPageIndex),
y: 0,
};
x: pageWidth * (1 - currentPageIndex),
y: 0,
};

return getMatrixByTranslate(translate);
}
Expand Down
32 changes: 25 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ export interface AxisTitleCfg {
* 文本对齐方式
* @type {string} start, center, end
*/
position?: string;
position?: string;
}

export interface BaseCfg {
Expand Down Expand Up @@ -593,6 +593,11 @@ export interface CategoryLegendCfg extends LegendBaseCfg {
* @type {LegendItemValueCfg}
*/
itemValue?: LegendItemValueCfg;
/**
*
* @type {LegendRadio}
*/
radio?: LegendRadio;
/**
* 最大宽度
* @type {number}
Expand All @@ -616,11 +621,11 @@ export interface CategoryLegendCfg extends LegendBaseCfg {
/**
* 是否翻页
*/
flipPage?: boolean;
/**
* 翻页行数(只适用于横向)
*/
maxRow?: number;
flipPage?: boolean;
/**
* 翻页行数(只适用于横向)
*/
maxRow?: number;
/**
* 分页器配置
* @type {LegendPageNavigatorCfg}
Expand Down Expand Up @@ -807,7 +812,20 @@ export interface LegendItemValueCfg {
* 图例项附加值的配置
* @type {ShapeAttrs}
*/
style?: ShapeAttrs | ShapeAttrsCallback;
style?: ShapeAttrs | ShapeAttrsCallback;
/**
* 图例值和后面的间隔,可以控制和 RadioIcon 的间距
* @type {number}
*/
spacing?: number;
}

export interface LegendRadio {
/**
* radio 的配置项
* @type {ShapeAttrs}
*/
style?: ShapeAttrs;
}

export interface LegendMarkerCfg {
Expand Down
124 changes: 124 additions & 0 deletions tests/unit/legend/category-radio-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Canvas } from '@antv/g-canvas';
import CategoryLegend from '../../../src/legend/category';

function createMountedDiv() {
const div = document.createElement('div');
document.body.appendChild(div);
return div;
}

function renderLegend(options = {}) {
const canvas = new Canvas({
width: 500,
height: 200,
container: createMountedDiv(),
});

const container = canvas.addGroup();

const legend = new CategoryLegend({
id: 'c',
container,
x: 100,
y: 100,
updateAutoRender: true,
itemBackground: null,
...options,
});

legend.init();
legend.render();

return legend;
}

describe('分类图例的正反选功能', () => {
test('radio === falsy 的时候不会渲染 radio', () => {
const legend = renderLegend({
items: [
{ name: 'a', value: 123, marker: { symbol: 'square', style: { r: 4, fill: '#5B8FF9' } } },
{ name: 'b', value: 223, marker: { symbol: 'square', style: { r: 4, fill: '#5AD8A6' } } },
{ name: 'c', value: 323, marker: { symbol: 'square', style: { r: 4, fill: '#5D7092' } } },
],
});

expect(legend.getElementsByName('legend-item-radio').length).toBe(0);
});

test('radio === truthy 会按照默认配置渲染 radio 并且只展示 show 为 true 的 item', () => {
const legend = renderLegend({
items: [
{ name: 'a', value: 123, marker: { symbol: 'square', style: { r: 4, fill: '#5B8FF9' } } },
{ name: 'b', value: 223, marker: { symbol: 'square', style: { r: 4, fill: '#5AD8A6' } }, showRadio: true },
{ name: 'c', value: 323, marker: { symbol: 'square', style: { r: 4, fill: '#5D7092' } } },
],
radio: {},
});

const radios = legend.getElementsByName('legend-item-radio');
const [hideRadio, showRadio] = radios;
expect(radios.length).toBe(3);
expect(hideRadio.attr('stroke')).toBe('#000000');
expect(hideRadio.attr('fill')).toBe('#ffffff');
expect(hideRadio.attr('opacity')).toBe(0);
expect(showRadio.attr('opacity')).toBe(0.45);
});

test('radio = {} 覆盖除了 opacity 之外的样式', () => {
const legend = renderLegend({
items: [
{ name: 'a', value: 123, marker: { symbol: 'square', style: { r: 4, fill: '#5B8FF9' } } },
{ name: 'b', value: 223, marker: { symbol: 'square', style: { r: 4, fill: '#5AD8A6' } }, showRadio: true },
{ name: 'c', value: 323, marker: { symbol: 'square', style: { r: 4, fill: '#5D7092' } } },
],
radio: {
opacity: 1,
fill: 'red',
stroke: 'blue',
},
});

const [radio] = legend.getElementsByName('legend-item-radio');
expect(radio.attr('stroke')).toBe('blue');
expect(radio.attr('fill')).toBe('red');
expect(radio.attr('opacity')).toBe(0);
});

test('radio 和 itemValue 的默认间距是 6', () => {
const legend = renderLegend({
items: [
{ name: 'a', value: 123, marker: { symbol: 'square', style: { r: 4, fill: '#5B8FF9' } }, showRadio: true },
{ name: 'b', value: 223, marker: { symbol: 'square', style: { r: 4, fill: '#5AD8A6' } } },
{ name: 'c', value: 323, marker: { symbol: 'square', style: { r: 4, fill: '#5D7092' } } },
],
radio: {},
itemValue: {},
});

const [radio] = legend.getElementsByName('legend-item-radio');
const [value] = legend.getElementsByName('legend-item-value');
const maxValueX = value.getBBox().maxX;
const minRadioX = radio.getBBox().x;
expect(minRadioX - maxValueX).toBe(4.792893218813447);
});

test('可以通过 itemValue.spacing 去配置 radio 和 itemValue 的间距', () => {
const legend = renderLegend({
items: [
{ name: 'a', value: 123, marker: { symbol: 'square', style: { r: 4, fill: '#5B8FF9' } }, showRadio: true },
{ name: 'b', value: 223, marker: { symbol: 'square', style: { r: 4, fill: '#5AD8A6' } } },
{ name: 'c', value: 323, marker: { symbol: 'square', style: { r: 4, fill: '#5D7092' } } },
],
radio: {},
itemValue: {
spacing: 10,
},
});

const [radio] = legend.getElementsByName('legend-item-radio');
const [value] = legend.getElementsByName('legend-item-value');
const maxValueX = value.getBBox().maxX;
const minRadioX = radio.getBBox().x;
expect(minRadioX - maxValueX).toBe(8.792893218813447);
});
});

0 comments on commit 2e8c756

Please sign in to comment.