Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: interval support cornerRadius(方案梳理中) #3170

Merged
merged 4 commits into from
Jan 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions examples/column/basic/demo/corner-radius.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Chart } from '@antv/g2';

const data = [
{ year: '1951 年', sales: 38 },
{ year: '1952 年', sales: 52 },
{ year: '1956 年', sales: 61 },
{ year: '1957 年', sales: 145 },
{ year: '1958 年', sales: 48 },
{ year: '1959 年', sales: 38 },
{ year: '1960 年', sales: 38 },
{ year: '1962 年', sales: 38 },
];
const chart = new Chart({
container: 'container',
autoFit: true,
height: 500,
});

chart.data(data);
chart.scale('sales', {
nice: true,
});

chart.tooltip({
showMarkers: false,
});
chart.interaction('active-region');

chart
.interval()
.position('year*sales')
.style({ radius: [20, 20, 0, 0] });

chart.render();
10 changes: 9 additions & 1 deletion examples/column/basic/demo/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@
"zh": "基础柱状图",
"en": "Column chart"
},
"screenshot": "https://gw.alipayobjects.com/mdn/rms_2274c3/afts/img/A*4JDKS4I7MvcAAAAAAAAAAABkARQnAQ"
"screenshot": "https://gw.alipayobjects.com/zos/antfincdn/e%24NnUtHbjm/a9632163-6aa8-4d53-8a2a-c1878426a51b.png"
},
{
"filename": "corner-radius.ts",
"title": {
"zh": "带倒角的柱状图",
"en": "Column chart with corner radius"
},
"screenshot": "https://gw.alipayobjects.com/zos/antfincdn/7asolORz4s/2781b4e3-3d12-43c7-8646-124e400d5182.png"
},
{
"filename": "ranged.ts",
Expand Down
10 changes: 8 additions & 2 deletions src/geometry/shape/interval/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Point, ShapeInfo, ShapeMarkerCfg, ShapePoint } from '../../../interface
import { registerShape, registerShapeFactory } from '../base';
import { BACKGROUND_SHAPE } from '../constant';
import { getBackgroundRectStyle, getStyle } from '../util/get-style';
import { getBackgroundRectPath, getIntervalRectPath, getRectPoints } from './util';
import { getBackgroundRectPath, getIntervalRectPath, getRectPoints, getRectWithCornerRadius } from './util';

/** Interval 的 shape 工厂 */
const IntervalShapeFactory = registerShapeFactory('interval', {
Expand Down Expand Up @@ -35,7 +35,13 @@ registerShape('interval', 'rect', {
});
}

const path = this.parsePath(getIntervalRectPath(cfg.points as Point[], style.lineCap, this.coordinate));
let path;
if (style.radius && this.coordinate.isRect) {
path = getRectWithCornerRadius(this.parsePoints(cfg.points), this.coordinate, style.radius);
} else {
path = this.parsePath(getIntervalRectPath(cfg.points as Point[], style.lineCap, this.coordinate));
}

const shape = group.addShape('path', {
attrs: {
...style,
Expand Down
69 changes: 69 additions & 0 deletions src/geometry/shape/interval/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,72 @@ export function getFunnelPath(points: Point[], nextPoints: Point[], isPyramid: b

return path;
}

/**
* 获取 倒角 矩形
* - 目前只适用于笛卡尔坐标系下
*/
export function getRectWithCornerRadius(points: Point[], coordinate: Coordinate, radius?: number | number[]) {
// 获取 四个关键点
let [p0, p1, p2, p3] = points;
let [r1, r2, r3, r4] = [0, 0, 0, 0];

/**
* p1 → p2
* ↑ ↓
* p0 ← p3
*
* 负数的情况,关键点会变成下面的形式
*
* p0 ← p3
* ↓ ↑
* p1 → p2
*/
if (p0.y < p1.y /** 负数情况 */) {
[p1, p0, p3, p2] = points;
[r4, r3, r2, r1] = parseRadius(radius, Math.min(p3.x - p0.x, p0.y - p1.y));
} else {
[r1, r2, r3, r4] = parseRadius(radius, Math.min(p3.x - p0.x, p0.y - p1.y));
}

/**
* 转置前
* p1 → p2
* ↑ ↓
* p0 ← p3
*
* 转置后(↓ 是 x 轴递增,→ 是 y 轴递增),从 p0 开始绘制,对应的 radius: [r3, r2, r1, r4]
* p3 ← p2
* ↓ ↑
* P0 → p1(points[3])
*
* 负数的情况,y 轴翻转
*
* p0 → p1
* ↑ ↓
* p3 ← p2
*/
if (coordinate.isTransposed) {
[p0, p3, p2, p1] = points;
if (points[0].x > points[1].x /** 负数情况 */) {
[p3, p0, p1, p2] = points;
[r1, r4, r3, r2] = parseRadius(radius, Math.min(p3.x - p0.x, p0.y - p1.y));
} else {
[r2, r3, r4, r1] = parseRadius(radius, Math.min(p3.x - p0.x, p0.y - p1.y));
}
}

const path = [];
path.push(['M', p1.x, p1.y + r1]);
r1 !== 0 && path.push(['A', r1, r1, 0, 0, 1, p1.x + r1, p1.y]);
path.push(['L', p2.x - r2, p2.y]);
r2 !== 0 && path.push(['A', r2, r2, 0, 0, 1, p2.x, p2.y + r2]);
path.push(['L', p3.x, p3.y - r3]);
r3 !== 0 && path.push(['A', r3, r3, 0, 0, 1, p3.x - r3, p3.y]);
path.push(['L', p0.x + r4, p0.y]);
r4 !== 0 && path.push(['A', r4, r4, 0, 0, 1, p0.x, p0.y - r4]);
path.push(['L', p1.x, p1.y + r1]);
path.push(['z']);

return path;
}
37 changes: 37 additions & 0 deletions tests/unit/geometry/shape/interval-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,43 @@ describe('Interval shapes', () => {
});
expect(lineCapRoundShapePath.length).toBe(6);
expect(lineCapRoundShapePathTypes).toEqual(['M', 'L', 'A', 'L', 'A', 'Z']);

// with corner-radius, priority is higher than lineCap
const rectWithCornerRadiusShape = IntervalShapeFactory.drawShape(
'rect',
{
...shapeCfg,
style: {
lineCap: 'round',
radius: 4
},
},
element.container
);
canvas.draw();
const path2 = rectWithCornerRadiusShape.attr('path');
let path2Types = [];
path2.forEach((path) => path2Types.push(path[0]));
expect(path2.length).toBe(10);
expect(path2Types).toEqual(['M', 'A', 'L', 'A', 'L', 'A', 'L', 'A', 'L', 'Z']);

// partial corner-radius
const rectWithCornerRadiusShape1 = IntervalShapeFactory.drawShape(
'rect',
{
...shapeCfg,
style: {
radius: [4, 4, 0, 0]
},
},
element.container
);
canvas.draw();
const path3 = rectWithCornerRadiusShape1.attr('path');
let path3Types = [];
path3.forEach((path) => path3Types.push(path[0]));
expect(path3.length).toBe(8);
expect(path3Types).toEqual(['M', 'A', 'L', 'A', 'L', 'L', 'L', 'Z']);
});

it('getMarker', () => {
Expand Down
177 changes: 176 additions & 1 deletion tests/unit/geometry/shape/interval-util-spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { getCoordinate } from '@antv/coord';
import { getBackgroundRectPath, parseRadius } from '../../../../src/geometry/shape/interval/util';
import {
getBackgroundRectPath,
parseRadius,
getRectWithCornerRadius,
} from '../../../../src/geometry/shape/interval/util';

const CartesianCoordinate = getCoordinate('rect');
const PolarCoordinate = getCoordinate('polar');
Expand Down Expand Up @@ -156,4 +160,175 @@ describe('绘制 interval shape 的一些 utils', () => {
// 极坐标系下 不支持background radius,因为两个 path 相等
expect(path2).toEqual(path);
});

it('直角坐标系:获取 带 corner-radius 的 rect', () => {
const rectCoord = new CartesianCoordinate(region);

const path = getRectWithCornerRadius(
[
{ x: 60, y: 150 },
{ x: 60, y: 0 },
{ x: 120, y: 0 },
{ x: 120, y: 150 },
],
rectCoord,
5
);
/**
* 从 p1 开始绘制,对应的 radius: [r1, r2, r3, r0]
* p1 ----> p2
* ↑ |
* | |
* | |
* | ↓
* P0 <---- P3
*/
expect(path).toEqual([
['M', 60, 5],
['A', 5, 5, 0, 0, 1, 65, 0],
['L', 115, 0],
['A', 5, 5, 0, 0, 1, 120, 5],
['L', 120, 145],
['A', 5, 5, 0, 0, 1, 115, 150],
['L', 65, 150],
['A', 5, 5, 0, 0, 1, 60, 145],
['L', 60, 5],
['z'],
]);

const path1 = getRectWithCornerRadius(
[
{ x: 60, y: 150 },
{ x: 60, y: 0 },
{ x: 120, y: 0 },
{ x: 120, y: 150 },
],
rectCoord,
[5, 0, 0, 5]
);
expect(path1).toEqual([
['M', 60, 5],
['A', 5, 5, 0, 0, 1, 65, 0],
['L', 120, 0],
['L', 120, 150],
['L', 65, 150],
['A', 5, 5, 0, 0, 1, 60, 145],
['L', 60, 5],
['z'],
]);
});

it('直角坐标系, 转置: 带 corner-radius 的 rect', () => {
const rectCoord = new CartesianCoordinate(region);
rectCoord.transpose();

const path = getRectWithCornerRadius(
[
{ x: 60, y: 150 },
{ x: 210, y: 150 },
{ x: 210, y: 90 },
{ x: 60, y: 90 },
],
rectCoord,
5
);
/**
* 转置后,
* 从 p3 开始绘制,对应的 radius: [r1, r2, r3, r0]
* p3 → p2
* ↑ ↓
* P0 ← P1
*/
expect(path).toEqual([
['M', 60, 95],
['A', 5, 5, 0, 0, 1, 65, 90],
['L', 205, 90],
['A', 5, 5, 0, 0, 1, 210, 95],
['L', 210, 145],
['A', 5, 5, 0, 0, 1, 205, 150],
['L', 65, 150],
['A', 5, 5, 0, 0, 1, 60, 145],
['L', 60, 95],
['z'],
]);


const path1 = getRectWithCornerRadius(
[
{ x: 60, y: 150 },
{ x: 210, y: 150 },
{ x: 210, y: 90 },
{ x: 60, y: 90 },
],
rectCoord,
[5, 0, 0, 5]
);
expect(path1).toEqual([
['M', 60, 95],
['A', 5, 5, 0, 0, 1, 65, 90],
['L', 205, 90],
['A', 5, 5, 0, 0, 1, 210, 95],
['L', 210, 150],
['L', 60, 150],
['L', 60, 95],
['z'],
]);
});

it('直角坐标系, 转置: 带负数值的,带 corner-radius 的 rect', () => {
const rectCoord = new CartesianCoordinate(region);
rectCoord.transpose();

const path = getRectWithCornerRadius(
[
{ x: 210, y: 150 },
{ x: 60, y: 150 },
{ x: 60, y: 90 },
{ x: 210, y: 90 },
],
rectCoord,
5
);
/**
* 转置后,
* 从 p3 开始绘制,对应的 radius: [r1, r2, r3, r0]
* p3 → p2
* ↑ ↓
* P0 ← P1
*/
expect(path).toEqual([
['M', 60, 95],
['A', 5, 5, 0, 0, 1, 65, 90],
['L', 205, 90],
['A', 5, 5, 0, 0, 1, 210, 95],
['L', 210, 145],
['A', 5, 5, 0, 0, 1, 205, 150],
['L', 65, 150],
['A', 5, 5, 0, 0, 1, 60, 145],
['L', 60, 95],
['z'],
]);


const path1 = getRectWithCornerRadius(
[
{ x: 210, y: 150 },
{ x: 60, y: 150 },
{ x: 60, y: 90 },
{ x: 210, y: 90 },
],
rectCoord,
[5, 0, 0, 5]
);
expect(path1).toEqual([
['M', 60, 95],
['A', 5, 5, 0, 0, 1, 65, 90],
['L', 205, 90],
['A', 5, 5, 0, 0, 1, 210, 95],
['L', 210, 150],
['L', 60, 150],
['L', 60, 95],
['z'],
]);
});
});