Skip to content

Commit 9c6197e

Browse files
Filmbostock
andauthored
extend interval to dot and text marks (#627)
* extend interval to dot, text, image marks * more idiomatic tests * Update README * only one-dimensional intervals Co-authored-by: Mike Bostock <[email protected]>
1 parent ddbe39b commit 9c6197e

File tree

9 files changed

+283
-9
lines changed

9 files changed

+283
-9
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,8 @@ Plot.dotX(cars.map(d => d["economy (mpg)"]))
10451045
10461046
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₂*, …].
10471047
1048+
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*.
1049+
10481050
#### Plot.dotY(*data*, *options*)
10491051
10501052
```js
@@ -1053,6 +1055,8 @@ Plot.dotY(cars.map(d => d["economy (mpg)"]))
10531055
10541056
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₂*, …].
10551057
1058+
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*.
1059+
10561060
### Hexgrid
10571061
10581062
The hexgrid mark can be used to support marks using the [hexbin](#hexbin) layout.
@@ -1327,10 +1331,14 @@ Returns a new text mark with the given *data* and *options*. If neither the **x*
13271331
13281332
Equivalent to [Plot.text](#plottextdata-options), except **x** defaults to the identity function and assumes that *data* = [*x₀*, *x₁*, *x₂*, …].
13291333
1334+
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*.
1335+
13301336
#### Plot.textY(*data*, *options*)
13311337
13321338
Equivalent to [Plot.text](#plottextdata-options), except **y** defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …].
13331339
1340+
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*.
1341+
13341342
### Tick
13351343
13361344
[<img src="./img/tick.png" width="320" height="198" alt="a barcode plot">](https://observablehq.com/@observablehq/plot-tick)

src/marks/dot.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {identity, maybeFrameAnchor, maybeNumberChannel, maybeTuple} from "../opt
44
import {Mark} from "../plot.js";
55
import {applyChannelStyles, applyDirectStyles, applyFrameAnchor, applyIndirectStyles, applyTransform, offset} from "../style.js";
66
import {maybeSymbolChannel} from "../symbols.js";
7+
import {maybeIntervalMidX, maybeIntervalMidY} from "../transforms/interval.js";
78

89
const defaults = {
910
ariaLabel: "dot",
@@ -95,11 +96,11 @@ export function dot(data, {x, y, ...options} = {}) {
9596
}
9697

9798
export function dotX(data, {x = identity, ...options} = {}) {
98-
return new Dot(data, {...options, x});
99+
return new Dot(data, maybeIntervalMidY({...options, x}));
99100
}
100101

101102
export function dotY(data, {y = identity, ...options} = {}) {
102-
return new Dot(data, {...options, y});
103+
return new Dot(data, maybeIntervalMidX({...options, y}));
103104
}
104105

105106
export function circle(data, options) {

src/marks/text.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {formatDefault} from "../format.js";
44
import {indexOf, identity, string, maybeNumberChannel, maybeTuple, numberChannel, isNumeric, isTemporal, keyword, maybeFrameAnchor, isTextual} from "../options.js";
55
import {Mark} from "../plot.js";
66
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyAttr, applyTransform, offset, impliedString, applyFrameAnchor} from "../style.js";
7+
import {maybeIntervalMidX, maybeIntervalMidY} from "../transforms/interval.js";
78

89
const defaults = {
910
ariaLabel: "text",
@@ -122,11 +123,11 @@ export function text(data, {x, y, ...options} = {}) {
122123
}
123124

124125
export function textX(data, {x = identity, ...options} = {}) {
125-
return new Text(data, {...options, x});
126+
return new Text(data, maybeIntervalMidY({...options, x}));
126127
}
127128

128129
export function textY(data, {y = identity, ...options} = {}) {
129-
return new Text(data, {...options, y});
130+
return new Text(data, maybeIntervalMidX({...options, y}));
130131
}
131132

132133
function applyIndirectTextStyles(selection, mark, T) {

src/transforms/interval.js

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {range} from "d3";
2-
import {labelof, maybeValue, valueof} from "../options.js";
2+
import {isTemporal, labelof, maybeValue, valueof} from "../options.js";
33
import {maybeInsetX, maybeInsetY} from "./inset.js";
44

55
// TODO Allow the interval to be specified as a string, e.g. “day” or “hour”?
@@ -43,13 +43,35 @@ function maybeIntervalK(k, maybeInsetK, options, trivial) {
4343
[`${k}2`]: v2 === undefined ? kv : v2
4444
};
4545
}
46-
let V1;
47-
const tv1 = data => V1 || (V1 = valueof(data, value).map(v => interval.floor(v)));
46+
let D1, V1;
47+
function transform(data) {
48+
if (V1 !== undefined && data === D1) return V1; // memoize
49+
return V1 = Array.from(valueof(D1 = data, value), v => interval.floor(v));
50+
}
4851
return maybeInsetK({
4952
...options,
5053
[k]: undefined,
51-
[`${k}1`]: v1 === undefined ? {transform: tv1, label} : v1,
52-
[`${k}2`]: v2 === undefined ? {transform: data => tv1(data).map(v => interval.offset(v)), label} : v2
54+
[`${k}1`]: v1 === undefined ? {transform, label} : v1,
55+
[`${k}2`]: v2 === undefined ? {transform: data => transform(data).map(v => interval.offset(v)), label} : v2
56+
});
57+
}
58+
59+
function maybeIntervalMidK(k, maybeInsetK, options) {
60+
const {[k]: v} = options;
61+
const {value, interval} = maybeIntervalValue(v, options);
62+
if (value == null || interval == null) return options;
63+
return maybeInsetK({
64+
...options,
65+
[k]: {
66+
label: labelof(v),
67+
transform: data => {
68+
const V1 = Array.from(valueof(data, value), v => interval.floor(v));
69+
const V2 = V1.map(v => interval.offset(v));
70+
return V1.map(isTemporal(V1)
71+
? (v1, v2) => v1 == null || isNaN(v1 = +v1) || (v2 = V2[v2], v2 == null) || isNaN(v2 = +v2) ? undefined : new Date((v1 + v2) / 2)
72+
: (v1, v2) => v1 == null || (v2 = V2[v2], v2 == null) ? NaN : (+v1 + +v2) / 2);
73+
}
74+
}
5375
});
5476
}
5577

@@ -68,3 +90,11 @@ export function maybeIntervalX(options = {}) {
6890
export function maybeIntervalY(options = {}) {
6991
return maybeIntervalK("y", maybeInsetY, options);
7092
}
93+
94+
export function maybeIntervalMidX(options = {}) {
95+
return maybeIntervalMidK("x", maybeInsetX, options);
96+
}
97+
98+
export function maybeIntervalMidY(options = {}) {
99+
return maybeIntervalMidK("y", maybeInsetY, options);
100+
}

test/output/athletesBirthdays.svg

Lines changed: 92 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)