diff --git a/README.md b/README.md index 710a602f3d..d767d99c27 100644 --- a/README.md +++ b/README.md @@ -511,6 +511,10 @@ All marks support the following style options: * **strokeDasharray** - a comma-separated list of dash lengths (in pixels) * **mixBlendMode** - the [blend mode](https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode) (*e.g.*, *multiply*) * **shapeRendering** - the [shape-rendering mode](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/shape-rendering) (*e.g.*, *crispEdges*) +* **dx** - horizontal offset (in pixels; defaults to 0) +* **dy** - vertical offset (in pixels; defaults to 0) + +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 high-density screens. All marks support the following optional channels: @@ -891,11 +895,9 @@ The following text-specific constant options are also supported: * **fontStyle** - the [font style](https://developer.mozilla.org/en-US/docs/Web/CSS/font-style); defaults to normal * **fontVariant** - the [font variant](https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant); defaults to normal * **fontWeight** - the [font weight](https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight); defaults to normal -* **dx** - the horizontal offset; defaults to 0 -* **dy** - the vertical offset; defaults to 0 * **rotate** - the rotation in degrees clockwise; defaults to 0 -The **dx** and **dy** options can be specified either as numbers representing pixels or as a string including units. For example, `"1em"` shifts the text by one [em](https://en.wikipedia.org/wiki/Em_(typography)), which is proportional to the **fontSize**. The **fontSize** and **rotate** options can be specified as either channels or constants. When fontSize or rotate is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. +For text marks, the **dx** and **dy** options can be specified either as numbers representing pixels or as a string including units. For example, `"1em"` shifts the text by one [em](https://en.wikipedia.org/wiki/Em_(typography)), which is proportional to the **fontSize**. The **fontSize** and **rotate** options can be specified as either channels or constants. When fontSize or rotate is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. #### Plot.text(*data*, *options*) diff --git a/src/mark.js b/src/mark.js index 463c1a3c53..2edcd8a548 100644 --- a/src/mark.js +++ b/src/mark.js @@ -13,7 +13,7 @@ const objectToString = Object.prototype.toString; export class Mark { constructor(data, channels = [], options = {}, defaults) { - const {facet = "auto", sort} = options; + const {facet = "auto", sort, dx, dy} = options; const names = new Set(); this.data = data; this.sort = isOptions(sort) ? sort : null; @@ -35,6 +35,8 @@ export class Mark { } return true; }); + this.dx = +dx || 0; + this.dy = +dy || 0; } initialize(facets, facetChannels) { let data = arrayify(this.data); diff --git a/src/marks/area.js b/src/marks/area.js index 15b85f18e0..1488eb5b6d 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -29,9 +29,10 @@ export class Area extends Mark { } render(I, {x, y}, channels) { const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, z: Z} = channels; + const {dx, dy} = this; return create("svg:g") .call(applyIndirectStyles, this) - .call(applyTransform, x, y) + .call(applyTransform, x, y, dx, dy) .call(g => g.selectAll() .data(Z ? group(I, i => Z[i]).values() : [I]) .join("path") diff --git a/src/marks/bar.js b/src/marks/bar.js index fc1c84c87c..9b223edadc 100644 --- a/src/marks/bar.js +++ b/src/marks/bar.js @@ -19,11 +19,11 @@ export class AbstractBar extends Mark { this.ry = impliedString(ry, "auto"); } render(I, scales, channels, dimensions) { - const {rx, ry} = this; + const {dx, dy, rx, ry} = this; const index = filter(I, ...this._positions(channels)); return create("svg:g") .call(applyIndirectStyles, this) - .call(this._transform, scales) + .call(this._transform, scales, dx, dy) .call(g => g.selectAll() .data(index) .join("rect") @@ -70,8 +70,8 @@ export class BarX extends AbstractBar { options ); } - _transform(selection, {x}) { - selection.call(applyTransform, x, null); + _transform(selection, {x}, dx, dy) { + selection.call(applyTransform, x, null, dx, dy); } _positions({x1: X1, x2: X2, y: Y}) { return [X1, X2, Y]; @@ -99,8 +99,8 @@ export class BarY extends AbstractBar { options ); } - _transform(selection, {y}) { - selection.call(applyTransform, null, y); + _transform(selection, {y}, dx, dy) { + selection.call(applyTransform, null, y, dx, dy); } _positions({y1: Y1, y2: Y2, x: X}) { return [Y1, Y2, X]; diff --git a/src/marks/dot.js b/src/marks/dot.js index 33158bfc10..2fdb88a3e7 100644 --- a/src/marks/dot.js +++ b/src/marks/dot.js @@ -1,7 +1,7 @@ import {create} from "d3"; import {filter, positive} from "../defined.js"; import {Mark, identity, maybeNumber, maybeTuple} from "../mark.js"; -import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js"; +import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js"; const defaults = { fill: "none", @@ -32,11 +32,12 @@ export class Dot extends Mark { {width, height, marginTop, marginRight, marginBottom, marginLeft} ) { const {x: X, y: Y, r: R} = channels; + const {dx, dy} = this; let index = filter(I, X, Y); if (R) index = index.filter(i => positive(R[i])); return create("svg:g") .call(applyIndirectStyles, this) - .call(applyTransform, x, y, 0.5, 0.5) + .call(applyTransform, x, y, offset + dx, offset + dy) .call(g => g.selectAll() .data(index) .join("circle") diff --git a/src/marks/frame.js b/src/marks/frame.js index 5e6d4a9108..8da9eba0c9 100644 --- a/src/marks/frame.js +++ b/src/marks/frame.js @@ -1,6 +1,6 @@ import {create} from "d3"; import {Mark, number} from "../mark.js"; -import {applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js"; +import {applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js"; const defaults = { fill: "none", @@ -24,11 +24,11 @@ export class Frame extends Mark { } render(I, scales, channels, dimensions) { const {marginTop, marginRight, marginBottom, marginLeft, width, height} = dimensions; - const {insetTop, insetRight, insetBottom, insetLeft} = this; + const {insetTop, insetRight, insetBottom, insetLeft, dx, dy} = this; return create("svg:rect") .call(applyIndirectStyles, this) .call(applyDirectStyles, this) - .call(applyTransform, null, null, 0.5, 0.5) + .call(applyTransform, null, null, offset + dx, offset + dy) .attr("x", marginLeft + insetLeft) .attr("y", marginTop + insetTop) .attr("width", width - marginLeft - marginRight - insetLeft - insetRight) diff --git a/src/marks/line.js b/src/marks/line.js index 20e93ac7f4..6630592993 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -2,7 +2,7 @@ import {create, group, line as shapeLine} from "d3"; import {Curve} from "../curve.js"; import {defined} from "../defined.js"; import {Mark, indexOf, identity, maybeTuple, maybeZ} from "../mark.js"; -import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js"; +import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, offset} from "../style.js"; const defaults = { fill: "none", @@ -28,9 +28,10 @@ export class Line extends Mark { } render(I, {x, y}, channels) { const {x: X, y: Y, z: Z} = channels; + const {dx, dy} = this; return create("svg:g") .call(applyIndirectStyles, this) - .call(applyTransform, x, y, 0.5, 0.5) + .call(applyTransform, x, y, offset + dx, offset + dy) .call(g => g.selectAll() .data(Z ? group(I, i => Z[i]).values() : [I]) .join("path") diff --git a/src/marks/link.js b/src/marks/link.js index 96997adb45..6fcdae8249 100644 --- a/src/marks/link.js +++ b/src/marks/link.js @@ -2,7 +2,7 @@ import {create, path} from "d3"; import {filter} from "../defined.js"; import {Mark} from "../mark.js"; import {Curve} from "../curve.js"; -import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js"; +import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js"; const defaults = { fill: "none", @@ -28,10 +28,11 @@ export class Link extends Mark { } render(I, {x, y}, channels) { const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels; + const {dx, dy} = this; const index = filter(I, X1, Y1, X2, Y2); return create("svg:g") .call(applyIndirectStyles, this) - .call(applyTransform, x, y, 0.5, 0.5) + .call(applyTransform, x, y, offset + dx, offset + dy) .call(g => g.selectAll() .data(index) .join("path") diff --git a/src/marks/rect.js b/src/marks/rect.js index fb648c9b7e..f303751771 100644 --- a/src/marks/rect.js +++ b/src/marks/rect.js @@ -43,11 +43,11 @@ export class Rect extends Mark { render(I, {x, y}, channels, dimensions) { const {x1: X1, y1: Y1, x2: X2, y2: Y2} = channels; const {marginTop, marginRight, marginBottom, marginLeft, width, height} = dimensions; - const {insetTop, insetRight, insetBottom, insetLeft, rx, ry} = this; + const {insetTop, insetRight, insetBottom, insetLeft, dx, dy, rx, ry} = this; const index = filter(I, X1, Y2, X2, Y2); return create("svg:g") .call(applyIndirectStyles, this) - .call(applyTransform, x, y) + .call(applyTransform, x, y, dx, dy) .call(g => g.selectAll() .data(index) .join("rect") diff --git a/src/marks/rule.js b/src/marks/rule.js index 68d5f2c28a..bfad5a0067 100644 --- a/src/marks/rule.js +++ b/src/marks/rule.js @@ -2,7 +2,7 @@ import {create} from "d3"; import {filter} from "../defined.js"; import {Mark, identity, number} from "../mark.js"; import {isCollapsed} from "../scales.js"; -import {applyDirectStyles, applyIndirectStyles, applyTransform, applyChannelStyles} from "../style.js"; +import {applyDirectStyles, applyIndirectStyles, applyTransform, applyChannelStyles, offset} from "../style.js"; const defaults = { fill: null, @@ -39,7 +39,7 @@ export class RuleX extends Mark { const index = filter(I, X, Y1, Y2); return create("svg:g") .call(applyIndirectStyles, this) - .call(applyTransform, X && x, null, 0.5, 0) + .call(applyTransform, X && x, null, offset, 0) .call(g => g.selectAll("line") .data(index) .join("line") @@ -79,11 +79,11 @@ export class RuleY extends Mark { render(I, {x, y}, channels, dimensions) { const {y: Y, x1: X1, x2: X2} = channels; const {width, height, marginTop, marginRight, marginLeft, marginBottom} = dimensions; - const {insetLeft, insetRight} = this; + const {insetLeft, insetRight, dx, dy} = this; const index = filter(I, Y, X1, X2); return create("svg:g") .call(applyIndirectStyles, this) - .call(applyTransform, null, Y && y, 0, 0.5) + .call(applyTransform, null, Y && y, dx, offset + dy) .call(g => g.selectAll("line") .data(index) .join("line") diff --git a/src/marks/text.js b/src/marks/text.js index d2c37618a3..c90376336a 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -1,7 +1,7 @@ import {create} from "d3"; import {filter, nonempty} from "../defined.js"; import {Mark, indexOf, identity, string, maybeNumber, maybeTuple, numberChannel} from "../mark.js"; -import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyAttr, applyTransform} from "../style.js"; +import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyAttr, applyTransform, offset} from "../style.js"; const defaults = {}; @@ -54,7 +54,7 @@ export class Text extends Mark { const cy = (marginTop + height - marginBottom) / 2; return create("svg:g") .call(applyIndirectTextStyles, this) - .call(applyTransform, x, y, 0.5, 0.5) + .call(applyTransform, x, y, offset, offset) .call(g => g.selectAll() .data(index) .join("text") diff --git a/src/marks/tick.js b/src/marks/tick.js index 5aadfbedff..429ee08928 100644 --- a/src/marks/tick.js +++ b/src/marks/tick.js @@ -1,7 +1,7 @@ import {create} from "d3"; import {filter} from "../defined.js"; import {Mark, identity, number} from "../mark.js"; -import {applyDirectStyles, applyIndirectStyles, applyTransform, applyChannelStyles} from "../style.js"; +import {applyDirectStyles, applyIndirectStyles, applyTransform, applyChannelStyles, offset} from "../style.js"; const defaults = { fill: null, @@ -14,10 +14,11 @@ class AbstractTick extends Mark { } render(I, scales, channels, dimensions) { const {x: X, y: Y} = channels; + const {dx, dy} = this; const index = filter(I, X, Y); return create("svg:g") .call(applyIndirectStyles, this) - .call(this._transform, scales) + .call(this._transform, scales, dx, dy) .call(g => g.selectAll("line") .data(index) .join("line") @@ -51,8 +52,8 @@ export class TickX extends AbstractTick { this.insetTop = number(insetTop); this.insetBottom = number(insetBottom); } - _transform(selection, {x}) { - selection.call(applyTransform, x, null, 0.5, 0); + _transform(selection, {x}, dx, dy) { + selection.call(applyTransform, x, null, offset + dx, dy); } _x1(scales, {x: X}) { return i => X[i]; @@ -90,8 +91,8 @@ export class TickY extends AbstractTick { this.insetRight = number(insetRight); this.insetLeft = number(insetLeft); } - _transform(selection, {y}) { - selection.call(applyTransform, null, y, 0, 0.5); + _transform(selection, {y}, dx, dy) { + selection.call(applyTransform, null, y, dx, offset + dy); } _x1(scales, {x: X}, {marginLeft}) { const {insetLeft} = this; diff --git a/src/style.js b/src/style.js index 57181e74b2..5bf1fcf4a9 100644 --- a/src/style.js +++ b/src/style.js @@ -131,8 +131,6 @@ export function applyStyle(selection, name, value) { } export function applyTransform(selection, x, y, tx, ty) { - tx = tx ? offset : 0; - ty = ty ? offset : 0; if (x && x.bandwidth) tx += x.bandwidth() / 2; if (y && y.bandwidth) ty += y.bandwidth() / 2; if (tx || ty) selection.attr("transform", `translate(${tx},${ty})`);