diff --git a/README.md b/README.md index 6fb96f2eb3..77a9e86fba 100644 --- a/README.md +++ b/README.md @@ -72,9 +72,9 @@ When drawing a single mark, you can call *mark*.**plot**(*options*) as shorthand ```js Plot.barY(alphabet, {x: "letter", y: "frequency"}).plot() ``` -### Layout options +### Geometry options -These options determine the overall layout of the plot; all are specified as numbers in pixels: +These options determine the overall geometry of the plot; all are specified as numbers in pixels: * **marginTop** - the top margin * **marginRight** - the right margin @@ -1824,6 +1824,35 @@ Plot.stackX2({y: "year", x: "revenue", z: "format", fill: "group"}) Equivalent to [Plot.stackX](#plotstackxstack-options), except that the **x2** channel is returned as the **x** channel. This can be used, for example, to draw a line at the right edge of each stacked area. +## Layouts + +A layout processes the transformed and scaled values of a mark before rendering. A layout might, for example, modify the marks’ positions to avoid occlusion. A layout operates in the representation space (such as pixels, *i.e.* after scales have been applied) rather than data space. + +### Dodge + +The dodge layout can be applied to the Dot, Text and Vector marks. + +#### Plot.dodgeY([*dodgeOptions*, ]*options*) + +```js +Plot.dodgeY({x: "date"}) +``` + +If the marks are arranged along the *x* axis, the dodgeY layout piles them vertically, keeping their *x* position unchanged, and creating a *y* position that avoids overlapping. + +#### Plot.dodgeX([*dodgeOptions*, ]*options*) + +```js +Plot.dodgeX({y: "value"}) +``` + +Equivalent to Plot.dodgeY, but the piling is horizontal, keeping the marks’ *y* position unchanged, and creating an *x* position that avoids overlapping. + +The dodge layouts accept the following options: + +* **padding** — a number of pixels added to the radius of the mark to estimate its size +* **anchor** - the layout’s anchor: one of *middle*, *right*, and *left* (default) for dodgeX, and one of *middle*, *top*, and *bottom* (default) for dodgeY. + ## Curves A curve defines how to turn a discrete representation of a line as a sequence of points [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] into a continuous path; *i.e.*, how to interpolate between points. Curves are used by the [line](#line), [area](#area), and [link](#link) mark, and are implemented by [d3-shape](https://github.com/d3/d3-shape/blob/master/README.md#curves). diff --git a/src/layouts/dodge.js b/src/layouts/dodge.js index 5e7371519b..8d1e9c382e 100644 --- a/src/layouts/dodge.js +++ b/src/layouts/dodge.js @@ -39,7 +39,7 @@ export function dodgeY(dodgeOptions = {}, options = {}) { } function dodge(y, x, anchor, padding, options) { - const [, r] = maybeNumberChannel(options.r, 3); + const [, r] = maybeNumberChannel(options.r, options.symbol ? 4.5 : 3); return layout(options, (I, scales, {[x]: X, r: R}, dimensions) => { if (X == null) throw new Error(`missing channel: ${x}`); let [ky, ty] = anchor(dimensions); diff --git a/src/marks/text.js b/src/marks/text.js index 31a2041075..e7bca92873 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -1,7 +1,7 @@ import {create, isoFormat, namespaces} from "d3"; -import {nonempty} from "../defined.js"; +import {nonempty, positive} from "../defined.js"; import {formatNumber} from "../format.js"; -import {indexOf, identity, string, maybeNumberChannel, maybeTuple, numberChannel, isNumeric, isTemporal, keyword, maybeFrameAnchor, isTextual} from "../options.js"; +import {indexOf, identity, string, maybeNumberChannel, maybeSymbolChannel, maybeTuple, numberChannel, isNumeric, isTemporal, keyword, maybeFrameAnchor, isTextual} from "../options.js"; import {Mark} from "../plot.js"; import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyAttr, applyTransform, offset, impliedString, applyFrameAnchor} from "../style.js"; @@ -28,10 +28,16 @@ export class Text extends Mark { fontStyle, fontVariant, fontWeight, - rotate + rotate, + symbol, + r } = options; const [vrotate, crotate] = maybeNumberChannel(rotate, 0); const [vfontSize, cfontSize] = maybeFontSizeChannel(fontSize); + + // compute the r channel if present, as it might be used by a layout (such as dodgeY) + const [vsymbol] = maybeSymbolChannel(symbol); + const [vr] = maybeNumberChannel(r, vsymbol == null ? 3 : 4.5); super( data, [ @@ -39,6 +45,7 @@ export class Text extends Mark { {name: "y", value: y, scale: "y", optional: true}, {name: "fontSize", value: vfontSize, optional: true}, {name: "rotate", value: numberChannel(vrotate), optional: true}, + {name: "r", value: vr, scale: "r", filter: positive, optional: true}, {name: "text", value: text, filter: nonempty} ], options, diff --git a/src/marks/vector.js b/src/marks/vector.js index 4f04d2c495..6e089eb8ae 100644 --- a/src/marks/vector.js +++ b/src/marks/vector.js @@ -1,6 +1,7 @@ import {create} from "d3"; import {radians} from "../math.js"; -import {maybeFrameAnchor, maybeNumberChannel, maybeTuple, keyword} from "../options.js"; +import {positive} from "../defined.js"; +import {maybeFrameAnchor, maybeNumberChannel, maybeSymbolChannel, maybeTuple, keyword} from "../options.js"; import {Mark} from "../plot.js"; import {applyChannelStyles, applyDirectStyles, applyFrameAnchor, applyIndirectStyles, applyTransform, offset} from "../style.js"; @@ -13,14 +14,19 @@ const defaults = { export class Vector extends Mark { constructor(data, options = {}) { - const {x, y, length, rotate, anchor = "middle", frameAnchor} = options; + const {x, y, length, rotate, anchor = "middle", frameAnchor, symbol, r} = options; const [vl, cl] = maybeNumberChannel(length, 12); const [vr, cr] = maybeNumberChannel(rotate, 0); + + // compute the r channel if present, as it might be used by a layout (such as dodgeY) + const [vsymbol] = maybeSymbolChannel(symbol); + const [vradius] = maybeNumberChannel(r, vsymbol == null ? 3 : 4.5); super( data, [ {name: "x", value: x, scale: "x", optional: true}, {name: "y", value: y, scale: "y", optional: true}, + {name: "r", value: vradius, scale: "r", filter: positive, optional: true}, {name: "length", value: vl, scale: "length", optional: true}, {name: "rotate", value: vr, optional: true} ], diff --git a/test/output/penguinDodge.svg b/test/output/penguinDodge.svg index 5383c14225..baa1080536 100644 --- a/test/output/penguinDodge.svg +++ b/test/output/penguinDodge.svg @@ -1,4 +1,4 @@ - + - + 3,000 @@ -36,348 +36,349 @@ 6,000 body_mass_g → - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG \ No newline at end of file diff --git a/test/output/penguinDodgeVector.svg b/test/output/penguinDodgeVector.svg new file mode 100644 index 0000000000..088d3f6143 --- /dev/null +++ b/test/output/penguinDodgeVector.svg @@ -0,0 +1,778 @@ + + + + + + 2,800 + + + + 3,000 + + + + 3,200 + + + + 3,400 + + + + 3,600 + + + + 3,800 + + + + 4,000 + + + + 4,200 + + + + 4,400 + + + + 4,600 + + + + 4,800 + + + + 5,000 + + + + 5,200 + + + + 5,400 + + + + 5,600 + + + + 5,800 + + + + 6,000 + + + + 6,200 + ↑ body_mass_g + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/index.js b/test/plots/index.js index df7cc1044e..7f13f62430 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -88,6 +88,7 @@ export {default as ordinalBar} from "./ordinal-bar.js"; export {default as penguinCulmen} from "./penguin-culmen.js"; export {default as penguinCulmenArray} from "./penguin-culmen-array.js"; export {default as penguinDodge} from "./penguin-dodge.js"; +export {default as penguinDodgeVector} from "./penguin-dodge-vector.js"; export {default as penguinFacetDodge} from "./penguin-facet-dodge.js"; export {default as penguinIslandUnknown} from "./penguin-island-unknown.js"; export {default as penguinMass} from "./penguin-mass.js"; diff --git a/test/plots/penguin-dodge-vector.js b/test/plots/penguin-dodge-vector.js new file mode 100644 index 0000000000..ea92f4f697 --- /dev/null +++ b/test/plots/penguin-dodge-vector.js @@ -0,0 +1,16 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const penguins = await d3.csv("data/penguins.csv", d3.autoType); + const r = d3.scaleOrdinal(new Set(penguins.map(d => d.species)), [-60, 0, 60]); + return Plot.plot({ + height: 450, + grid: true, + marks: [ + Plot.dot(penguins, Plot.dodgeX({padding: 2, anchor: "middle"}, {y: "body_mass_g", fill: "sex", r: 7})), + Plot.vector(penguins, Plot.dodgeX({padding: 2, anchor: "middle"}, {y: "body_mass_g", rotate: d => r(d.species), r: 7, length: 5})) + ], + color: {scheme: "pastel2"} + }); +} diff --git a/test/plots/penguin-dodge.js b/test/plots/penguin-dodge.js index 47fc6ce892..83210fb3a0 100644 --- a/test/plots/penguin-dodge.js +++ b/test/plots/penguin-dodge.js @@ -4,9 +4,10 @@ import * as d3 from "d3"; export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ - height: 200, + height: 400, marks: [ - Plot.dot(penguins, Plot.dodgeY({x: "body_mass_g"})) + Plot.dot(penguins, Plot.dodgeY({x: "body_mass_g", fill: "black", r: 7})), + Plot.text(penguins, Plot.dodgeY({x: "body_mass_g", text: d => d.species?.[0], r: 7, fill: "white"})) ] }); }