Skip to content

circular images, clip css basic shapes #1421

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

Closed
wants to merge 4 commits into from
Closed
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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ All marks support the following style options:
* **pointerEvents** - the [pointer events](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events) (*e.g.*, *none*)
* **clip** - whether and how to clip the mark

If the **clip** option is *frame* (or equivalently true), the mark is clipped to the frame’s dimensions; if the **clip** option is null (or equivalently false), the mark is not clipped. If the **clip** option is *sphere*, then a [geographic projection](#projection-options) is required and the mark will be clipped to the projected sphere (_e.g._, the front hemisphere when using the orthographic projection).
If the **clip** option is *frame* (or equivalently true), the mark is clipped to the frame’s dimensions; if the **clip** option is null (or equivalently false), the mark is not clipped. If the **clip** option is *sphere*, then a [geographic projection](#projection-options) is required and the mark will be clipped to the projected sphere (_e.g._, the front hemisphere when using the orthographic projection). The **clip** option can also be specified as a CSS [basic-shape](https://developer.mozilla.org/en-US/docs/Web/CSS/basic-shape).

For all marks except [text](#plottextdata-options), the **dx** and **dy** options are rendered as a transform property, possibly including a 0.5px offset on low-density screens.

Expand Down Expand Up @@ -1529,10 +1529,13 @@ In addition to the [standard mark options](#marks), the following optional chann
* **y** - the vertical position; bound to the *y* scale
* **width** - the image width (in pixels)
* **height** - the image height (in pixels)
* **r** - the image radius, for rounded images; bound to the *r* scale

If either of the **x** or **y** channels are not specified, the corresponding position is controlled by the **frameAnchor** option.

The **width** and **height** options default to 16 pixels and can be specified as either a channel or constant. When the width or height is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. Images with a nonpositive width or height are not drawn. If a **width** is specified but not a **height**, or *vice versa*, the one defaults to the other. Images do not support either a fill or a stroke.
The **width** and **height** options default to 16 pixels (unless **r** is specified) and can be specified as either a channel or constant. When the width or height is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. Images with a nonpositive width or height are not drawn. If a **width** is specified but not a **height**, or *vice versa*, the one defaults to the other. Images do not support either a fill or a stroke.

The **r** option, if specified as a constant or a channel, defaults **clip** to *circle()*, and defaults **width** and **height** to twice the effective radius, thus allowing to draw the image as a disc of the given radius.

The following image-specific constant options are also supported:

Expand Down
5 changes: 4 additions & 1 deletion src/mark.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,11 +273,14 @@ export interface MarkOptions {
*
* - *frame* or true - clip to the plot’s frame (inner area)
* - *sphere* - clip to the projected sphere (*e.g.*, front hemisphere)
* - a CSS [basic shape][1], such as *circle()*
* - null or false - do not clip
*
* The *sphere* clip option requires a geographic projection.
*
* [1]: https://developer.mozilla.org/en-US/docs/Web/CSS/basic-shape
*/
clip?: "frame" | "sphere" | boolean | null;
clip?: "frame" | "sphere" | "circle()" | (string & Record<never, never>) | boolean | null;

/**
* The horizontal offset in pixels; a constant option. On low-density screens,
Expand Down
2 changes: 1 addition & 1 deletion src/mark.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export class Mark {
this.marginRight = +marginRight;
this.marginBottom = +marginBottom;
this.marginLeft = +marginLeft;
this.clip = maybeClip(clip);
[this.clip, this.clipShape] = maybeClip(clip);
// Super-faceting currently disallow position channels; in the future, we
// could allow position to be specified in fx and fy in addition to (or
// instead of) x and y.
Expand Down
9 changes: 9 additions & 0 deletions src/marks/image.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ export interface ImageOptions extends MarkOptions {
*/
height?: ChannelValue;

/**
* The image radius, for rounded images. When a number, it is interpreted as a
* constant radius in pixels; otherwise it is interpreted as a channel,
* typically bound to the *r* scale. Also sets the default **clip** option to
* _circle()_, and the default **height** and **width** to twice its effective
* value.
*/
r?: ChannelValue;

/**
* The required image URL (or relative path). If a string that starts with a
* dot, slash, or URL protocol (*e.g.*, “https:”) it is assumed to be a
Expand Down
38 changes: 23 additions & 15 deletions src/marks/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,35 +40,43 @@ function maybePathChannel(value) {

export class Image extends Mark {
constructor(data, options = {}) {
let {x, y, width, height, src, preserveAspectRatio, crossOrigin, frameAnchor, imageRendering} = options;
let {x, y, r, width, height, src, preserveAspectRatio, crossOrigin, frameAnchor, imageRendering} = options;
if (width === undefined && height !== undefined) width = height;
else if (height === undefined && width !== undefined) height = width;
const [vs, cs] = maybePathChannel(src);
const [vw, cw] = maybeNumberChannel(width, 16);
const [vh, ch] = maybeNumberChannel(height, 16);
const [vr, cr] = maybeNumberChannel(r);
const [vw, cw] = maybeNumberChannel(width, r != null ? cr : 16);
const [vh, ch] = maybeNumberChannel(height, r != null ? cr : 16);
super(
data,
{
x: {value: x, scale: "x", optional: true},
y: {value: y, scale: "y", optional: true},
r: {value: vr, scale: "r", filter: positive, optional: true},
width: {value: vw, filter: positive, optional: true},
height: {value: vh, filter: positive, optional: true},
src: {value: vs, optional: true}
},
options,
{
...(r != null && {clip: "circle()"}),
...options
},
defaults
);
this.src = cs;
this.width = cw;
this.height = ch;
this.sw = width == null && r != null ? 1 : 1 / 2;
this.sh = height == null && r != null ? 1 : 1 / 2;
this.preserveAspectRatio = impliedString(preserveAspectRatio, "xMidYMid");
this.crossOrigin = string(crossOrigin);
this.frameAnchor = maybeFrameAnchor(frameAnchor);
this.imageRendering = impliedString(imageRendering, "auto");
}
render(index, scales, channels, dimensions, context) {
const {x, y} = scales;
const {x: X, y: Y, width: W, height: H, src: S} = channels;
const {width, height, sw, sh} = this;
const {x: X, y: Y, r: R, width: W = width == null && R, height: H = height == null && R, src: S} = channels;
const [cx, cy] = applyFrameAnchor(this, dimensions);
return create("svg:g", context)
.call(applyIndirectStyles, this, dimensions, context)
Expand All @@ -83,25 +91,25 @@ export class Image extends Mark {
.attr(
"x",
W && X
? (i) => X[i] - W[i] / 2
? (i) => X[i] - W[i] * sw
: W
? (i) => cx - W[i] / 2
? (i) => cx - W[i] * sw
: X
? (i) => X[i] - this.width / 2
: cx - this.width / 2
? (i) => X[i] - width * sw
: cx - width * sw
)
.attr(
"y",
H && Y
? (i) => Y[i] - H[i] / 2
? (i) => Y[i] - H[i] * sh
: H
? (i) => cy - H[i] / 2
? (i) => cy - H[i] * sh
: Y
? (i) => Y[i] - this.height / 2
: cy - this.height / 2
? (i) => Y[i] - height * sh
: cy - height * sh
)
.attr("width", W ? (i) => W[i] : this.width)
.attr("height", H ? (i) => H[i] : this.height)
.attr("width", W ? (i) => W[i] * 2 * sw : width * 2 * sw)
.attr("height", H ? (i) => H[i] * 2 * sh : height * 2 * sh)
.call(applyAttr, "href", S ? (i) => S[i] : this.src)
.call(applyAttr, "preserveAspectRatio", this.preserveAspectRatio)
.call(applyAttr, "crossorigin", this.crossOrigin)
Expand Down
5 changes: 4 additions & 1 deletion src/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,9 @@ export function* groupIndex(I, position, {z}, channels) {
export function maybeClip(clip) {
if (clip === true) clip = "frame";
else if (clip === false) clip = null;
return maybeKeyword(clip, "clip", ["frame", "sphere"]);
return typeof clip === "string" && /^(circle|ellipse|inset|polygon|rectangle)\([^)]*\)$/.test(clip)
? [, clip]
: [maybeKeyword(clip, "clip", ["frame", "sphere"])];
}

// Note: may mutate selection.node!
Expand Down Expand Up @@ -374,6 +376,7 @@ export function applyIndirectStyles(selection, mark, dimensions, context) {

export function applyDirectStyles(selection, mark) {
applyStyle(selection, "mix-blend-mode", mark.mixBlendMode);
applyStyle(selection, "clip-path", mark.clipShape);
applyAttr(selection, "opacity", mark.opacity);
}

Expand Down
Loading