diff --git a/README.md b/README.md index d4960a78a6..a356261d07 100644 --- a/README.md +++ b/README.md @@ -1331,10 +1331,11 @@ 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) +* **rotate** - the image rotation angle (in degrees, defaults to 0) 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 and can be specified as either a channel or constant. When the width, height, or rotate 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 following image-specific constant options are also supported: diff --git a/src/marks/image.js b/src/marks/image.js index 1039e7256c..7f0a7d5609 100644 --- a/src/marks/image.js +++ b/src/marks/image.js @@ -40,12 +40,13 @@ function maybePathChannel(value) { export class Image extends Mark { constructor(data, options = {}) { - let {x, y, width, height, src, preserveAspectRatio, crossOrigin, frameAnchor} = options; + let {x, y, width, height, rotate, src, preserveAspectRatio, crossOrigin, frameAnchor} = 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 [vrotate, crotate] = maybeNumberChannel(rotate, 0); super( data, { @@ -53,6 +54,7 @@ export class Image extends Mark { y: {value: y, scale: "y", optional: true}, width: {value: vw, filter: positive, optional: true}, height: {value: vh, filter: positive, optional: true}, + rotate: {value: vrotate, optional: true}, src: {value: vs, optional: true} }, options, @@ -61,13 +63,14 @@ export class Image extends Mark { this.src = cs; this.width = cw; this.height = ch; + this.rotate = crotate; this.preserveAspectRatio = impliedString(preserveAspectRatio, "xMidYMid"); this.crossOrigin = string(crossOrigin); this.frameAnchor = maybeFrameAnchor(frameAnchor); } render(index, scales, channels, dimensions, context) { const {x, y} = scales; - const {x: X, y: Y, width: W, height: H, src: S} = channels; + const {x: X, y: Y, width: W, height: H, rotate: R, src: S} = channels; const [cx, cy] = applyFrameAnchor(this, dimensions); return create("svg:g", context) .call(applyIndirectStyles, this, scales, dimensions, context) @@ -105,6 +108,19 @@ export class Image extends Mark { .call(applyAttr, "preserveAspectRatio", this.preserveAspectRatio) .call(applyAttr, "crossorigin", this.crossOrigin) .call(applyChannelStyles, this, channels) + .attr("transform", R ? (i) => `rotate(${R[i]})` : this.rotate ? `rotate(${this.rotate})` : null) + .attr( + "transform-origin", + R || this.rotate + ? X && Y + ? (i) => `${X[i]}px ${Y[i]}px` + : X + ? (i) => `${X[i]}px ${cy}px` + : Y + ? (i) => `${cx}px ${Y[i]}px` + : `${cx}px ${cy}px` + : null + ) ) .node(); }