Skip to content

Commit

Permalink
extend interval to dot and text marks (observablehq#627)
Browse files Browse the repository at this point in the history
* extend interval to dot, text, image marks

* more idiomatic tests

* Update README

* only one-dimensional intervals

Co-authored-by: Mike Bostock <mbostock@gmail.com>
  • Loading branch information
Fil and mbostock authored May 29, 2022
1 parent ddbe39b commit 9c6197e
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 9 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,8 @@ Plot.dotX(cars.map(d => d["economy (mpg)"]))
Equivalent to [Plot.dot](#plotdotdata-options) except that if the **x** option is not specified, it defaults to the identity function and assumes that *data* = [*x₀*, *x₁*, *x₂*, …].
If an **interval** is specified, such as d3.utcDay, **y** is transformed to (*interval*.floor(*y*) + *interval*.offset(*interval*.floor(*y*))) / 2. If the interval is specified as a number *n*, *y* will be the midpoint of two consecutive multiples of *n* that bracket *y*.
#### Plot.dotY(*data*, *options*)
```js
Expand All @@ -1053,6 +1055,8 @@ Plot.dotY(cars.map(d => d["economy (mpg)"]))
Equivalent to [Plot.dot](#plotdotdata-options) except that if the **y** option is not specified, it defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …].
If an **interval** is specified, such as d3.utcDay, **x** is transformed to (*interval*.floor(*x*) + *interval*.offset(*interval*.floor(*x*))) / 2. If the interval is specified as a number *n*, *x* will be the midpoint of two consecutive multiples of *n* that bracket *x*.
### Hexgrid
The hexgrid mark can be used to support marks using the [hexbin](#hexbin) layout.
Expand Down Expand Up @@ -1327,10 +1331,14 @@ Returns a new text mark with the given *data* and *options*. If neither the **x*
Equivalent to [Plot.text](#plottextdata-options), except **x** defaults to the identity function and assumes that *data* = [*x₀*, *x₁*, *x₂*, …].
If an **interval** is specified, such as d3.utcDay, **y** is transformed to (*interval*.floor(*y*) + *interval*.offset(*interval*.floor(*y*))) / 2. If the interval is specified as a number *n*, *y* will be the midpoint of two consecutive multiples of *n* that bracket *y*.
#### Plot.textY(*data*, *options*)
Equivalent to [Plot.text](#plottextdata-options), except **y** defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …].
If an **interval** is specified, such as d3.utcDay, **x** is transformed to (*interval*.floor(*x*) + *interval*.offset(*interval*.floor(*x*))) / 2. If the interval is specified as a number *n*, *x* will be the midpoint of two consecutive multiples of *n* that bracket *x*.
### Tick
[<img src="./img/tick.png" width="320" height="198" alt="a barcode plot">](https://observablehq.com/@observablehq/plot-tick)
Expand Down
5 changes: 3 additions & 2 deletions src/marks/dot.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {identity, maybeFrameAnchor, maybeNumberChannel, maybeTuple} from "../opt
import {Mark} from "../plot.js";
import {applyChannelStyles, applyDirectStyles, applyFrameAnchor, applyIndirectStyles, applyTransform, offset} from "../style.js";
import {maybeSymbolChannel} from "../symbols.js";
import {maybeIntervalMidX, maybeIntervalMidY} from "../transforms/interval.js";

const defaults = {
ariaLabel: "dot",
Expand Down Expand Up @@ -95,11 +96,11 @@ export function dot(data, {x, y, ...options} = {}) {
}

export function dotX(data, {x = identity, ...options} = {}) {
return new Dot(data, {...options, x});
return new Dot(data, maybeIntervalMidY({...options, x}));
}

export function dotY(data, {y = identity, ...options} = {}) {
return new Dot(data, {...options, y});
return new Dot(data, maybeIntervalMidX({...options, y}));
}

export function circle(data, options) {
Expand Down
5 changes: 3 additions & 2 deletions src/marks/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {formatDefault} from "../format.js";
import {indexOf, identity, string, maybeNumberChannel, 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";
import {maybeIntervalMidX, maybeIntervalMidY} from "../transforms/interval.js";

const defaults = {
ariaLabel: "text",
Expand Down Expand Up @@ -122,11 +123,11 @@ export function text(data, {x, y, ...options} = {}) {
}

export function textX(data, {x = identity, ...options} = {}) {
return new Text(data, {...options, x});
return new Text(data, maybeIntervalMidY({...options, x}));
}

export function textY(data, {y = identity, ...options} = {}) {
return new Text(data, {...options, y});
return new Text(data, maybeIntervalMidX({...options, y}));
}

function applyIndirectTextStyles(selection, mark, T) {
Expand Down
40 changes: 35 additions & 5 deletions src/transforms/interval.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {range} from "d3";
import {labelof, maybeValue, valueof} from "../options.js";
import {isTemporal, labelof, maybeValue, valueof} from "../options.js";
import {maybeInsetX, maybeInsetY} from "./inset.js";

// TODO Allow the interval to be specified as a string, e.g. “day” or “hour”?
Expand Down Expand Up @@ -43,13 +43,35 @@ function maybeIntervalK(k, maybeInsetK, options, trivial) {
[`${k}2`]: v2 === undefined ? kv : v2
};
}
let V1;
const tv1 = data => V1 || (V1 = valueof(data, value).map(v => interval.floor(v)));
let D1, V1;
function transform(data) {
if (V1 !== undefined && data === D1) return V1; // memoize
return V1 = Array.from(valueof(D1 = data, value), v => interval.floor(v));
}
return maybeInsetK({
...options,
[k]: undefined,
[`${k}1`]: v1 === undefined ? {transform: tv1, label} : v1,
[`${k}2`]: v2 === undefined ? {transform: data => tv1(data).map(v => interval.offset(v)), label} : v2
[`${k}1`]: v1 === undefined ? {transform, label} : v1,
[`${k}2`]: v2 === undefined ? {transform: data => transform(data).map(v => interval.offset(v)), label} : v2
});
}

function maybeIntervalMidK(k, maybeInsetK, options) {
const {[k]: v} = options;
const {value, interval} = maybeIntervalValue(v, options);
if (value == null || interval == null) return options;
return maybeInsetK({
...options,
[k]: {
label: labelof(v),
transform: data => {
const V1 = Array.from(valueof(data, value), v => interval.floor(v));
const V2 = V1.map(v => interval.offset(v));
return V1.map(isTemporal(V1)
? (v1, v2) => v1 == null || isNaN(v1 = +v1) || (v2 = V2[v2], v2 == null) || isNaN(v2 = +v2) ? undefined : new Date((v1 + v2) / 2)
: (v1, v2) => v1 == null || (v2 = V2[v2], v2 == null) ? NaN : (+v1 + +v2) / 2);
}
}
});
}

Expand All @@ -68,3 +90,11 @@ export function maybeIntervalX(options = {}) {
export function maybeIntervalY(options = {}) {
return maybeIntervalK("y", maybeInsetY, options);
}

export function maybeIntervalMidX(options = {}) {
return maybeIntervalMidK("x", maybeInsetX, options);
}

export function maybeIntervalMidY(options = {}) {
return maybeIntervalMidK("y", maybeInsetY, options);
}
92 changes: 92 additions & 0 deletions test/output/athletesBirthdays.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 9c6197e

Please sign in to comment.