Skip to content

rounded rect #2099

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

Merged
merged 22 commits into from
Jul 21, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
44 changes: 42 additions & 2 deletions src/marks/rect.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ export interface RectCornerOptions {
/**
* The rounded corner [*x*-radius][1], either in pixels or as a percentage of
* the rect width. If **rx** is not specified, it defaults to **ry** if
* present, and otherwise draws square corners.
* present, and otherwise draws square corners. This option is ignored if a
* more specific corner radius (one of **rx1y1**, **rx2y1**, **rx1y2**, or
* **rx2y2**) is specified.
*
* [1]: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/rx
*/
Expand All @@ -18,11 +20,49 @@ export interface RectCornerOptions {
/**
* The rounded corner [*y*-radius][1], either in pixels or as a percentage of
* the rect height. If **ry** is not specified, it defaults to **rx** if
* present, and otherwise draws square corners.
* present, and otherwise draws square corners. This option is ignored if a
* more specific corner radius (one of **rx1y1**, **rx2y1**, **rx1y2**, or
* **rx2y2**) is specified.
*
* [1]: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/ry
*/
ry?: number | string;

/**
* The rounded radius for the **x1** corners, in pixels; shorthand for
* **rx1y1** and **rx1y2**.
*/
rx1?: number;

/**
* The rounded radius for the **y1** corners, in pixels; shorthand for
* **rx1y1** and **rx2y1**.
*/
ry1?: number;

/**
* The rounded radius for the **x2** corners, in pixels; shorthand for
* **rx2y1** and **rx2y2**.
*/
rx2?: number;

/**
* The rounded radius for the **y2** corners, in pixels; shorthand for
* **rx1y2** and **rx2y2**.
*/
ry2?: number;

/** The rounded radius for the **x1y1** corner, in pixels. */
rx1y1?: number;

/** The rounded radius for the **x1y2** corner, in pixels. */
rx1y2?: number;

/** The rounded radius for the **x2y1** corner, in pixels. */
rx2y1?: number;

/** The rounded radius for the **x2y2** corner, in pixels. */
rx2y2?: number;
}

/** Options for the rect mark. */
Expand Down
155 changes: 112 additions & 43 deletions src/marks/rect.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ export class Rect extends Mark {
insetBottom = inset,
insetLeft = inset,
rx,
ry
ry,
rx1,
ry1,
rx2,
ry2,
rx1y1 = rx1 !== undefined ? +rx1 : ry1 !== undefined ? +ry1 : 0,
rx1y2 = rx1 !== undefined ? +rx1 : ry2 !== undefined ? +ry2 : 0,
rx2y1 = rx2 !== undefined ? +rx2 : ry1 !== undefined ? +ry1 : 0,
rx2y2 = rx2 !== undefined ? +rx2 : ry2 !== undefined ? +ry2 : 0
} = options;
super(
data,
Expand All @@ -42,17 +50,27 @@ export class Rect extends Mark {
this.insetRight = number(insetRight);
this.insetBottom = number(insetBottom);
this.insetLeft = number(insetLeft);
this.rx = impliedString(rx, "auto"); // number or percentage
this.ry = impliedString(ry, "auto");
if (rx1y1 || rx1y2 || rx2y1 || rx2y2) {
this.rx1y1 = Math.max(0, rx1y1);
this.rx1y2 = Math.max(0, rx1y2);
this.rx2y1 = Math.max(0, rx2y1);
this.rx2y2 = Math.max(0, rx2y2);
} else {
this.rx = impliedString(rx, "auto"); // number or percentage
this.ry = impliedString(ry, "auto");
}
}
render(index, scales, channels, dimensions, context) {
const {x, y} = scales;
const {x1: X1, y1: Y1, x2: X2, y2: Y2} = channels;
let {x1: X1, y1: Y1, x2: X2, y2: Y2} = channels;
const {marginTop, marginRight, marginBottom, marginLeft, width, height} = dimensions;
const {projection} = context;
const {insetTop, insetRight, insetBottom, insetLeft, rx, ry} = this;
const bx = (x?.bandwidth ? x.bandwidth() : 0) - insetLeft - insetRight;
const by = (y?.bandwidth ? y.bandwidth() : 0) - insetTop - insetBottom;
const {insetTop, insetRight, insetBottom, insetLeft} = this;
const {rx, ry, rx1y1, rx1y2, rx2y1, rx2y2} = this;
if ((X1 || X2) && !projection && isCollapsed(x)) X1 = X2 = null; // ignore if collapsed
if ((Y1 || Y2) && !projection && isCollapsed(y)) Y1 = Y2 = null; // ignore if collapsed
const bx = x?.bandwidth ? x.bandwidth() : 0;
const by = y?.bandwidth ? y.bandwidth() : 0;
return create("svg:g", context)
.call(applyIndirectStyles, this, dimensions, context)
.call(applyTransform, this, {}, 0, 0)
Expand All @@ -61,48 +79,99 @@ export class Rect extends Mark {
.selectAll()
.data(index)
.enter()
.append("rect")
.call(applyDirectStyles, this)
.attr(
"x",
X1 && (projection || !isCollapsed(x))
? X2
? (i) => Math.min(X1[i], X2[i]) + insetLeft
: (i) => X1[i] + insetLeft
: marginLeft + insetLeft
.call(
rx1y1 || rx1y2 || rx2y1 || rx2y2
? (g) =>
g
.append("path")
.call(applyDirectStyles, this)
.call(
applyRoundedRect,
this,
X1 ? (i) => X1[i] : () => marginLeft,
Y1 ? (i) => Y1[i] : () => marginTop,
X1 ? (X2 ? (i) => X2[i] : (i) => X1[i] + bx) : () => width - marginRight,
Y1 ? (Y2 ? (i) => Y2[i] : (i) => Y1[i] + by) : () => height - marginBottom
)
.call(applyChannelStyles, this, channels)
: (g) =>
g
.append("rect")
.call(applyDirectStyles, this)
.attr(
"x",
X1
? X2
? (i) => Math.min(X1[i], X2[i]) + insetLeft
: (i) => X1[i] + insetLeft
: marginLeft + insetLeft
)
.attr(
"y",
Y1
? Y2
? (i) => Math.min(Y1[i], Y2[i]) + insetTop
: (i) => Y1[i] + insetTop
: marginTop + insetTop
)
.attr(
"width",
X1
? X2
? (i) => Math.max(0, Math.abs(X2[i] - X1[i]) + bx - insetLeft - insetRight)
: bx - insetLeft - insetRight
: width - marginRight - marginLeft - insetRight - insetLeft
)
.attr(
"height",
Y1
? Y2
? (i) => Math.max(0, Math.abs(Y1[i] - Y2[i]) + by - insetTop - insetBottom)
: by - insetTop - insetBottom
: height - marginTop - marginBottom - insetTop - insetBottom
)
.call(applyAttr, "rx", rx)
.call(applyAttr, "ry", ry)
.call(applyChannelStyles, this, channels)
)
.attr(
"y",
Y1 && (projection || !isCollapsed(y))
? Y2
? (i) => Math.min(Y1[i], Y2[i]) + insetTop
: (i) => Y1[i] + insetTop
: marginTop + insetTop
)
.attr(
"width",
X1 && (projection || !isCollapsed(x))
? X2
? (i) => Math.max(0, Math.abs(X2[i] - X1[i]) + bx)
: bx
: width - marginRight - marginLeft - insetRight - insetLeft
)
.attr(
"height",
Y1 && (projection || !isCollapsed(y))
? Y2
? (i) => Math.max(0, Math.abs(Y1[i] - Y2[i]) + by)
: by
: height - marginTop - marginBottom - insetTop - insetBottom
)
.call(applyAttr, "rx", rx)
.call(applyAttr, "ry", ry)
.call(applyChannelStyles, this, channels)
)
.node();
}
}

export function applyRoundedRect(selection, mark, X1, Y1, X2, Y2) {
const {insetTop, insetRight, insetBottom, insetLeft} = mark;
const {rx1y1, rx1y2, rx2y1, rx2y2} = mark;
selection.attr("d", (i) => {
const x1i = X1(i);
const y1i = Y1(i);
const x2i = X2(i);
const y2i = Y2(i);
const ix = x1i > x2i;
const iy = y1i > y2i;
const x1 = (ix ? x2i : x1i) + insetLeft;
const x2 = (ix ? x1i : x2i) - insetRight;
const y1 = (iy ? y2i : y1i) + insetTop;
const y2 = (iy ? y1i : y2i) - insetBottom;
const k = Math.min(
1,
(x2 - x1) / Math.max(rx1y1 + rx2y1, rx1y2 + rx2y2),
(y2 - y1) / Math.max(rx1y1 + rx1y2, rx2y1 + rx2y2)
);
const rx1y1i = k * (ix ? (iy ? rx2y2 : rx2y1) : iy ? rx1y2 : rx1y1);
const rx2y1i = k * (ix ? (iy ? rx1y2 : rx1y1) : iy ? rx2y2 : rx2y1);
const rx2y2i = k * (ix ? (iy ? rx1y1 : rx1y2) : iy ? rx2y1 : rx2y2);
const rx1y2i = k * (ix ? (iy ? rx2y1 : rx2y2) : iy ? rx1y1 : rx1y2);
return (
`M${x1},${y1 + rx1y1i}A${rx1y1i},${rx1y1i} 0 0 1 ${x1 + rx1y1i},${y1}` +
`H${x2 - rx2y1i}A${rx2y1i},${rx2y1i} 0 0 1 ${x2},${y1 + rx2y1i}` +
`V${y2 - rx2y2i}A${rx2y2i},${rx2y2i} 0 0 1 ${x2 - rx2y2i},${y2}` +
`H${x1 + rx1y2i}A${rx1y2i},${rx1y2i} 0 0 1 ${x1},${y2 - rx1y2i}` +
`Z`
);
});
}

export function rect(data, options) {
return new Rect(data, maybeTrivialIntervalX(maybeTrivialIntervalY(options)));
}
Expand Down
67 changes: 67 additions & 0 deletions test/output/roundedRectAsymmetricX.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
67 changes: 67 additions & 0 deletions test/output/roundedRectAsymmetricY.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading