Skip to content
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
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -839,10 +839,17 @@ In addition to the [standard mark options](#marks), the following optional chann
* **x** - the horizontal position; bound to the *x* scale
* **y** - the vertical position; bound to the *y* scale
* **r** - the radius (area); bound to the *radius* scale, which defaults to *sqrt*
* **rotate** - the rotation angle in degrees clockwise; defaults to 0
* **symbol** - the categorical symbol; bound to the *symbol* scale; defaults to circle
* **rotate** - the rotation angle in degrees clockwise
* **symbol** - the categorical symbol; bound to the *symbol* scale

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

The following dot-specific constant options are also supported:

If the **x** channel is not specified, dots will be horizontally centered in the plot (or facet). Likewise if the **y** channel is not specified, dots will be vertically centered in the plot (or facet). Typically either *x*, *y*, or both are specified.
* **r** - the effective radius (length); a number in pixels
* **rotate** - the rotation angle in degrees clockwise; defaults to 0
* **symbol** - the categorical symbol; defaults to circle
* **frameAnchor** - the frame anchor; top-left, top, top-right, right, bottom-right, bottom, bottom-left, left, or middle (default)

The **r** option can be specified as either a channel or constant. When the radius is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. The radius defaults to 4.5 pixels when using the **symbol** channel, and otherwise 3 pixels. Dots with a nonpositive radius are not drawn.

Expand All @@ -858,7 +865,7 @@ Dots are drawn in input order, with the last data drawn on top. If sorting is ne
Plot.dot(sales, {x: "units", y: "fruit"})
```

Returns a new dot with the given *data* and *options*. If neither the **x** nor **y** options are specified, *data* is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].
Returns a new dot with the given *data* and *options*. If neither the **x** nor **y** nor **frameAnchor** options are specified, *data* is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].

#### Plot.dotX(*data*, *options*)

Expand Down Expand Up @@ -889,11 +896,17 @@ In addition to the [standard mark options](#marks), the following optional chann
* **width** - the image width (in pixels)
* **height** - the image height (in pixels)

If the **x** channel is not specified, images will be horizontally centered in the plot (or facet). Likewise if the **y** channel is not specified, images will be vertically centered in the plot (or facet). Typically either *x*, *y*, or both are specified.
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 **preserveAspectRatio** and **crossOrigin** options, both constant, allow control over the [aspect ratio](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio) and [cross-origin](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/crossorigin) behavior, respectively. The default aspect ratio behavior is “xMidYMid meet”; consider “xMidYMid slice” to crop the image instead of scaling it to fit.
The following image-specific constant options are also supported:

* **frameAnchor** - the frame anchor; top-left, top, top-right, right, bottom-right, bottom, bottom-left, left, or middle (default)
* **preserveAspectRatio** - the [aspect ratio](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio); defaults to “xMidYMid meet”
* **crossOrigin** - the [cross-origin](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/crossorigin) behavior

To crop the image instead of scaling it to fit, set **preserveAspectRatio** to “xMidYMid slice”.

Images are drawn in input order, with the last data drawn on top. If sorting is needed, say to mitigate overplotting, consider a [sort and reverse transform](#transforms).

Expand All @@ -903,7 +916,7 @@ Images are drawn in input order, with the last data drawn on top. If sorting is
Plot.image(presidents, {x: "inauguration", y: "favorability", src: "portrait"})
```

Returns a new image with the given *data* and *options*. If neither the **x** nor **y** options are specified, *data* is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].
Returns a new image with the given *data* and *options*. If neither the **x** nor **y** nor **frameAnchor** options are specified, *data* is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].

### Line

Expand Down Expand Up @@ -1085,6 +1098,8 @@ In addition to the [standard mark options](#marks), the following optional chann
* **fontSize** - the font size in pixels
* **rotate** - the rotation angle in degrees clockwise

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

The following text-specific constant options are also supported:

* **textAnchor** - the [text anchor](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor) for horizontal position; start, end, or middle (default)
Expand All @@ -1095,13 +1110,14 @@ 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
* **frameAnchor** - the frame anchor; top-left, top, top-right, right, bottom-right, bottom, bottom-left, left, or middle (default)
* **rotate** - the rotation angle in degrees clockwise; defaults to 0

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*)

Returns a new text mark with the given *data* and *options*. If neither the **x** nor **y** options are specified, *data* is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].
Returns a new text mark with the given *data* and *options*. If neither the **x** nor **y** nor **frameAnchor** options are specified, *data* is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].

#### Plot.textX(*data*, *options*)

Expand Down
19 changes: 7 additions & 12 deletions src/marks/dot.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {create, path, symbolCircle} from "d3";
import {positive} from "../defined.js";
import {identity, maybeNumberChannel, maybeSymbolChannel, maybeTuple} from "../options.js";
import {identity, maybeFrameAnchor, maybeNumberChannel, maybeSymbolChannel, maybeTuple} from "../options.js";
import {Mark} from "../plot.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js";
import {applyChannelStyles, applyDirectStyles, applyFrameAnchor, applyIndirectStyles, applyTransform, offset} from "../style.js";

const defaults = {
fill: "none",
Expand All @@ -12,7 +12,7 @@ const defaults = {

export class Dot extends Mark {
constructor(data, options = {}) {
const {x, y, r, rotate, symbol = symbolCircle} = options;
const {x, y, r, rotate, symbol = symbolCircle, frameAnchor} = options;
const [vrotate, crotate] = maybeNumberChannel(rotate, 0);
const [vsymbol, csymbol] = maybeSymbolChannel(symbol);
const [vr, cr] = maybeNumberChannel(r, vsymbol == null ? 3 : 4.5);
Expand All @@ -31,6 +31,7 @@ export class Dot extends Mark {
this.r = cr;
this.rotate = crotate;
this.symbol = csymbol;
this.frameAnchor = maybeFrameAnchor(frameAnchor);

// Give a hint to the symbol scale; this allows the symbol scale to chose
// appropriate default symbols based on whether the dots are filled or
Expand All @@ -46,16 +47,10 @@ export class Dot extends Mark {
};
}
}
render(
index,
{x, y},
channels,
{width, height, marginTop, marginRight, marginBottom, marginLeft}
) {
render(index, {x, y}, channels, dimensions) {
const {x: X, y: Y, r: R, rotate: A, symbol: S} = channels;
const {dx, dy} = this;
const cx = (marginLeft + width - marginRight) / 2;
const cy = (marginTop + height - marginBottom) / 2;
const [cx, cy] = applyFrameAnchor(this, dimensions);
const circle = this.symbol === symbolCircle;
return create("svg:g")
.call(applyIndirectStyles, this)
Expand Down Expand Up @@ -92,7 +87,7 @@ export class Dot extends Mark {
}

export function dot(data, {x, y, ...options} = {}) {
([x, y] = maybeTuple(x, y));
if (options.frameAnchor === undefined) ([x, y] = maybeTuple(x, y));
return new Dot(data, {...options, x, y});
}

Expand Down
19 changes: 7 additions & 12 deletions src/marks/image.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {create} from "d3";
import {positive} from "../defined.js";
import {maybeNumberChannel, maybeTuple, string} from "../options.js";
import {maybeFrameAnchor, maybeNumberChannel, maybeTuple, string} from "../options.js";
import {Mark} from "../plot.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, applyAttr, offset, impliedString} from "../style.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, applyAttr, offset, impliedString, applyFrameAnchor} from "../style.js";

const defaults = {
fill: null,
Expand Down Expand Up @@ -33,7 +33,7 @@ function maybePathChannel(value) {

export class Image extends Mark {
constructor(data, options = {}) {
let {x, y, width, height, src, preserveAspectRatio, crossOrigin} = options;
let {x, y, width, height, 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);
Expand All @@ -56,17 +56,12 @@ export class Image extends Mark {
this.height = ch;
this.preserveAspectRatio = impliedString(preserveAspectRatio, "xMidYMid");
this.crossOrigin = string(crossOrigin);
this.frameAnchor = maybeFrameAnchor(frameAnchor);
}
render(
index,
{x, y},
channels,
{width, height, marginTop, marginRight, marginBottom, marginLeft}
) {
render(index, {x, y}, channels, dimensions) {
const {x: X, y: Y, width: W, height: H, src: S} = channels;
const cx = (marginLeft + width - marginRight) / 2;
const cy = (marginTop + height - marginBottom) / 2;
const {dx, dy} = this;
const [cx, cy] = applyFrameAnchor(this, dimensions);
return create("svg:g")
.call(applyIndirectStyles, this)
.call(applyTransform, x, y, offset + dx, offset + dy)
Expand All @@ -87,6 +82,6 @@ export class Image extends Mark {
}

export function image(data, {x, y, ...options} = {}) {
([x, y] = maybeTuple(x, y));
if (options.frameAnchor === undefined) ([x, y] = maybeTuple(x, y));
return new Image(data, {...options, x, y});
}
23 changes: 13 additions & 10 deletions src/marks/text.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {create, isoFormat, namespaces} from "d3";
import {nonempty} from "../defined.js";
import {formatNumber} from "../format.js";
import {indexOf, identity, string, maybeNumberChannel, maybeTuple, numberChannel, isNumeric, isTemporal, keyword} from "../options.js";
import {indexOf, identity, string, maybeNumberChannel, maybeTuple, numberChannel, isNumeric, isTemporal, keyword, maybeFrameAnchor} from "../options.js";
import {Mark} from "../plot.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyAttr, applyTransform, offset, impliedString} from "../style.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyAttr, applyTransform, offset, impliedString, applyFrameAnchor} from "../style.js";

const defaults = {
strokeLinejoin: "round"
Expand All @@ -23,6 +23,7 @@ export class Text extends Mark {
fontStyle,
fontVariant,
fontWeight,
frameAnchor,
rotate
} = options;
const [vrotate, crotate] = maybeNumberChannel(rotate, 0);
Expand All @@ -48,13 +49,12 @@ export class Text extends Mark {
this.fontStyle = string(fontStyle);
this.fontVariant = string(fontVariant);
this.fontWeight = string(fontWeight);
this.frameAnchor = maybeFrameAnchor(frameAnchor);
}
render(index, {x, y}, channels, dimensions) {
const {x: X, y: Y, rotate: R, text: T, fontSize: FS} = channels;
const {width, height, marginTop, marginRight, marginBottom, marginLeft} = dimensions;
const {dx, dy, rotate} = this;
const cx = (marginLeft + width - marginRight) / 2;
const cy = (marginTop + height - marginBottom) / 2;
const [cx, cy] = applyFrameAnchor(this, dimensions);
return create("svg:g")
.call(applyIndirectTextStyles, this, T)
.call(applyTransform, x, y, offset + dx, offset + dy)
Expand All @@ -63,15 +63,18 @@ export class Text extends Mark {
.join("text")
.call(applyDirectStyles, this)
.call(applyMultilineText, this, T)
.call(R ? text => text.attr("transform", X && Y ? i => `translate(${X[i]},${Y[i]}) rotate(${R[i]})`
.attr("transform", R ? (X && Y ? i => `translate(${X[i]},${Y[i]}) rotate(${R[i]})`
: X ? i => `translate(${X[i]},${cy}) rotate(${R[i]})`
: Y ? i => `translate(${cx},${Y[i]}) rotate(${R[i]})`
: i => `translate(${cx},${cy}) rotate(${R[i]})`)
: rotate ? text => text.attr("transform", X && Y ? i => `translate(${X[i]},${Y[i]}) rotate(${rotate})`
: rotate ? (X && Y ? i => `translate(${X[i]},${Y[i]}) rotate(${rotate})`
: X ? i => `translate(${X[i]},${cy}) rotate(${rotate})`
: Y ? i => `translate(${cx},${Y[i]}) rotate(${rotate})`
: `translate(${cx},${cy}) rotate(${rotate})`)
: text => text.attr("x", X ? i => X[i] : cx).attr("y", Y ? i => Y[i] : cy))
: (X && Y ? i => `translate(${X[i]},${Y[i]})`
: X ? i => `translate(${X[i]},${cy})`
: Y ? i => `translate(${cx},${Y[i]})`
: `translate(${cx},${cy})`))
.call(applyAttr, "font-size", FS && (i => FS[i]))
.call(applyChannelStyles, this, channels))
.node();
Expand All @@ -95,14 +98,14 @@ function applyMultilineText(selection, {lineAnchor, lineHeight}, T) {
this.appendChild(tspan);
}
} else {
if (y) this.setAttribute("dy", `${y * lineHeight}em`);
if (y) this.setAttribute("y", `${y * lineHeight}em`);
this.textContent = lines[0];
}
});
}

export function text(data, {x, y, ...options} = {}) {
([x, y] = maybeTuple(x, y));
if (options.frameAnchor === undefined) ([x, y] = maybeTuple(x, y));
return new Text(data, {...options, x, y});
}

Expand Down
4 changes: 4 additions & 0 deletions src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ export function maybeSymbolChannel(symbol) {
return [symbol, undefined];
}

export function maybeFrameAnchor(value = "middle") {
return keyword(value, "frameAnchor", ["middle", "top-left", "top", "top-right", "right", "bottom-right", "bottom", "bottom-left", "left"]);
}

// Like a sort comparator, returns a positive value if the given array of values
// is in ascending order, a negative value if the values are in descending
// order. Assumes monotonicity; only tests the first and last values.
Expand Down
7 changes: 7 additions & 0 deletions src/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,10 @@ export function applyInlineStyles(selection, style) {
}
}
}

export function applyFrameAnchor({frameAnchor}, {width, height, marginTop, marginRight, marginBottom, marginLeft}) {
return [
/left$/.test(frameAnchor) ? marginLeft : /right$/.test(frameAnchor) ? width - marginRight : (marginLeft + width - marginRight) / 2,
/^top/.test(frameAnchor) ? marginTop : /^bottom/.test(frameAnchor) ? height - marginBottom : (marginTop + height - marginBottom) / 2
];
}
Loading