Skip to content
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

frameAnchor; fix multiline text #686

Merged
merged 4 commits into from
Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
26 changes: 21 additions & 5 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 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 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,6 +1110,7 @@ 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.
Expand Down
17 changes: 6 additions & 11 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
17 changes: 6 additions & 11 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 Down
10 changes: 5 additions & 5 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 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need the -? here since the keywords have already been validated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do, because you can have e.g. “top” or “top-left” and we want to match on both.

Copy link
Member Author

@mbostock mbostock Jan 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see what you mean, because /^top/ will match both, and nothing else. That’s true, but I was a little worried that we’d introduce another name in the future that would coincidentally match the same prefix. I think it’s clearer to have the hyphen as an explicit delimiter even though it’s not strictly necessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant that matching "top" or "left" seems to be enough. Just a nit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I thought about /^top($|-)/ instead, but we’d have to change this code if we introduce new frameAnchor values anyway, so it feels fine to take your suggestion.

];
}