Skip to content

Commit 33ab79e

Browse files
mbostockFil
andauthored
interval for rect (#550)
* interval for rect * default insets for intervals * Update src/transforms/interval.js * numeric interval & documentation (#552) * interval for rule and bar * Update CHANGELOG Co-authored-by: Philippe Rivière <[email protected]>
1 parent ce19abb commit 33ab79e

File tree

11 files changed

+301
-42
lines changed

11 files changed

+301
-42
lines changed

CHANGELOG.md

+19-11
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,40 @@
11
# Observable Plot - Changelog
22

3-
## 0.2.1
3+
## 0.2.3
4+
5+
*Not yet released.* These notes are a work in progress.
6+
7+
Rect, bar, and rule marks now accept an *interval* option that allows to derive *x1* and *x2* from *x*, or *y1* and *y2* from *y*, where appropriate. A typical use case is for data that represents a fixed time interval; for example, using d3.utcDay as the interval creates rects that span a whole day, from UTC midnight to UTC midnight, that contains the associated time instant. The interval must be specifed as an object with two methods: **floor**(*x*) returns the start of the interval *x1* for the given *x*, while **offset**(*x*) returns the end of the interval *x2* for the given interval start *x*. If the interval is specified as a number, *x1* and *x2* are taken as the two consecutive multiples of *n* that bracket *x*.
8+
9+
## 0.2.2
410

511
Released September 19, 2021.
612

7-
### Marks
13+
Fix a crash with the axis.tickRotate option when there are no ticks to rotate.
814

9-
The constant *dx* and *dy* options have been extended to all marks, allowing to shift the mark by *dx* pixels horizontally and *dy* pixels vertically. Since only text elements accept the dx and dy properties, in all the other marks these are rendered as a transform (2D transformation) property of the mark’s parent, possibly including a 0.5px offset on low-density screens.
15+
## 0.2.1
1016

11-
### Scales
17+
Released September 19, 2021.
1218

13-
Quantitative scales, as well as identity position scales, now coerce channel values to numbers; both null and undefined are coerced to NaN. Similarly, time scales coerce channel values to dates; numbers are assumed to be milliseconds since UNIX epoch, while strings are assumed to be in [ISO 8601 format](https://github.com/mbostock/isoformat/blob/main/README.md#parsedate-fallback).
19+
The constant *dx* and *dy* options have been extended to all marks, allowing to shift the mark by *dx* pixels horizontally and *dy* pixels vertically. Since only text elements accept the dx and dy properties, in all the other marks these are rendered as a transform (2D transformation) property of the mark’s parent, possibly including a 0.5px offset on low-density screens.
1420

15-
### Transforms
21+
Quantitative scales, as well as identity position scales, now coerce channel values to numbers; both null and undefined are coerced to NaN. Similarly, time scales coerce channel values to dates; numbers are assumed to be milliseconds since UNIX epoch, while strings are assumed to be in [ISO 8601 format](https://github.com/mbostock/isoformat/blob/main/README.md#parsedate-fallback).
1622

17-
#### Plot.bin
23+
Bin transform reducers now receive the extent of the current bin as an argument after the data. For example, it allows to create meaningful titles:
1824

19-
The reducers now receive the extent of the current bin as an argument after the data. For example, it allows to create meaningful titles:
2025
```js
2126
Plot.rect(
2227
athletes,
2328
Plot.bin(
2429
{
2530
fill: "count",
26-
title: (bin, { x1, x2, y1, y2 }) =>
27-
`${bin.length} athletes weighing between ${x1} and ${x2} and with a height between ${y1} and ${y2}`
31+
title: (bin, {x1, x2, y1, y2}) => `${bin.length} athletes weighing between ${x1} and ${x2} and with a height between ${y1} and ${y2}`
2832
},
29-
{ x: "weight", y: "height", inset: 0 }
33+
{
34+
x: "weight",
35+
y: "height",
36+
inset: 0
37+
}
3038
)
3139
).plot()
3240
```

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,9 @@ The following channels are optional:
801801
* **x2** - the ending horizontal position; bound to the *x* scale
802802
* **y2** - the ending vertical position; bound to the *y* scale
803803

804-
Typically either **x1** and **x2** are specified, or **y1** and **y2**, or both. The rect mark supports the [standard mark options](#marks), including insets and rounded corners. The **stroke** defaults to none. The **fill** defaults to currentColor if the stroke is none, and to none otherwise.
804+
Typically either **x1** and **x2** are specified, or **y1** and **y2**, or both. **x1** and **x2** can be derived from **x** and an **interval** object (such as d3.utcDay) with a **floor** method that returns *x1* from *x* and an **offset** method that returns *x2* from *x1*. If the interval is specified as a number *n*, *x1* and *x2* are taken as the two consecutive multiples of *n* that bracket *x*. The interval may be specified either as as {x, interval} or x: {value, interval}—typically to apply different intervals to x and y.
805+
806+
The rect mark supports the [standard mark options](#marks), including insets and rounded corners. The **stroke** defaults to none. The **fill** defaults to currentColor if the stroke is none, and to none otherwise.
805807

806808
#### Plot.rect(*data*, *options*)
807809

src/marks/bar.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {filter} from "../defined.js";
33
import {Mark, number} from "../mark.js";
44
import {isCollapsed} from "../scales.js";
55
import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js";
6+
import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js";
67
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
78

89
const defaults = {};
@@ -116,9 +117,9 @@ export class BarY extends AbstractBar {
116117
}
117118

118119
export function barX(data, options) {
119-
return new BarX(data, maybeStackX(options));
120+
return new BarX(data, maybeStackX(maybeIntervalX(options)));
120121
}
121122

122123
export function barY(data, options) {
123-
return new BarY(data, maybeStackY(options));
124+
return new BarY(data, maybeStackY(maybeIntervalY(options)));
124125
}

src/marks/rect.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {filter} from "../defined.js";
33
import {Mark, number} from "../mark.js";
44
import {isCollapsed} from "../scales.js";
55
import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js";
6+
import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js";
67
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
78

89
const defaults = {};
@@ -64,13 +65,13 @@ export class Rect extends Mark {
6465
}
6566

6667
export function rect(data, options) {
67-
return new Rect(data, options);
68+
return new Rect(data, maybeIntervalX(maybeIntervalY(options)));
6869
}
6970

7071
export function rectX(data, options) {
71-
return new Rect(data, maybeStackX(options));
72+
return new Rect(data, maybeStackX(maybeIntervalY(options)));
7273
}
7374

7475
export function rectY(data, options) {
75-
return new Rect(data, maybeStackY(options));
76+
return new Rect(data, maybeStackY(maybeIntervalX(options)));
7677
}

src/marks/rule.js

+7-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {filter} from "../defined.js";
33
import {Mark, identity, number} from "../mark.js";
44
import {isCollapsed} from "../scales.js";
55
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyChannelStyles, offset} from "../style.js";
6+
import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js";
67

78
const defaults = {
89
fill: null,
@@ -97,14 +98,16 @@ export class RuleY extends Mark {
9798
}
9899
}
99100

100-
export function ruleX(data, {x = identity, y, y1, y2, ...options} = {}) {
101+
export function ruleX(data, options) {
102+
let {x = identity, y, y1, y2, ...rest} = maybeIntervalY(options);
101103
([y1, y2] = maybeOptionalZero(y, y1, y2));
102-
return new RuleX(data, {...options, x, y1, y2});
104+
return new RuleX(data, {...rest, x, y1, y2});
103105
}
104106

105-
export function ruleY(data, {y = identity, x, x1, x2, ...options} = {}) {
107+
export function ruleY(data, options) {
108+
let {y = identity, x, x1, x2, ...rest} = maybeIntervalX(options);
106109
([x1, x2] = maybeOptionalZero(x, x1, x2));
107-
return new RuleY(data, {...options, y, x1, x2});
110+
return new RuleY(data, {...rest, y, x1, x2});
108111
}
109112

110113
// For marks specified either as [0, x] or [x1, x2], or nothing.

src/transforms/bin.js

+9-21
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,25 @@
11
import {bin as binner, extent, thresholdFreedmanDiaconis, thresholdScott, thresholdSturges, utcTickInterval} from "d3";
22
import {valueof, range, identity, maybeLazyChannel, maybeTuple, maybeColor, maybeValue, mid, labelof, isTemporal} from "../mark.js";
3-
import {offset} from "../style.js";
43
import {basic} from "./basic.js";
54
import {maybeEvaluator, maybeGroup, maybeOutput, maybeOutputs, maybeReduce, maybeSort, maybeSubgroup, reduceCount, reduceIdentity} from "./group.js";
5+
import {maybeInsetX, maybeInsetY} from "./inset.js";
66

77
// Group on {z, fill, stroke}, then optionally on y, then bin x.
8-
export function binX(outputs = {y: "count"}, {inset, insetLeft, insetRight, ...options} = {}) {
9-
let {x, y} = options;
10-
x = maybeBinValue(x, options, identity);
11-
([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight));
12-
return binn(x, null, null, y, outputs, {inset, insetLeft, insetRight, ...options});
8+
export function binX(outputs = {y: "count"}, options = {}) {
9+
const {x, y} = options;
10+
return binn(maybeBinValue(x, options, identity), null, null, y, outputs, maybeInsetX(options));
1311
}
1412

1513
// Group on {z, fill, stroke}, then optionally on x, then bin y.
16-
export function binY(outputs = {x: "count"}, {inset, insetTop, insetBottom, ...options} = {}) {
17-
let {x, y} = options;
18-
y = maybeBinValue(y, options, identity);
19-
([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom));
20-
return binn(null, y, x, null, outputs, {inset, insetTop, insetBottom, ...options});
14+
export function binY(outputs = {x: "count"}, options = {}) {
15+
const {x, y} = options;
16+
return binn(null, maybeBinValue(y, options, identity), x, null, outputs, maybeInsetY(options));
2117
}
2218

2319
// Group on {z, fill, stroke}, then bin on x and y.
24-
export function bin(outputs = {fill: "count"}, {inset, insetTop, insetRight, insetBottom, insetLeft, ...options} = {}) {
20+
export function bin(outputs = {fill: "count"}, options = {}) {
2521
const {x, y} = maybeBinValueTuple(options);
26-
([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom));
27-
([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight));
28-
return binn(x, y, null, null, outputs, {inset, insetTop, insetRight, insetBottom, insetLeft, ...options});
22+
return binn(x, y, null, null, outputs, maybeInsetX(maybeInsetY(options)));
2923
}
3024

3125
function binn(
@@ -252,9 +246,3 @@ function binfilter([{x0, x1}, set]) {
252246
function binempty() {
253247
return new Uint32Array(0);
254248
}
255-
256-
function maybeInset(inset, inset1, inset2) {
257-
return inset === undefined && inset1 === undefined && inset2 === undefined
258-
? (offset ? [1, 0] : [0.5, 0.5])
259-
: [inset1, inset2];
260-
}

src/transforms/inset.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {offset} from "../style.js";
2+
3+
export function maybeInsetX({inset, insetLeft, insetRight, ...options} = {}) {
4+
([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight));
5+
return {inset, insetLeft, insetRight, ...options};
6+
}
7+
8+
export function maybeInsetY({inset, insetTop, insetBottom, ...options} = {}) {
9+
([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom));
10+
return {inset, insetTop, insetBottom, ...options};
11+
}
12+
13+
function maybeInset(inset, inset1, inset2) {
14+
return inset === undefined && inset1 === undefined && inset2 === undefined
15+
? (offset ? [1, 0] : [0.5, 0.5])
16+
: [inset1, inset2];
17+
}

src/transforms/interval.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {labelof, maybeValue, valueof} from "../mark.js";
2+
import {maybeInsetX, maybeInsetY} from "./inset.js";
3+
4+
// TODO Allow the interval to be specified as a string, e.g. “day” or “hour”?
5+
// This will require the interval knowing the type of the associated scale to
6+
// chose between UTC and local time (or better, an explicit timeZone option).
7+
function maybeInterval(interval) {
8+
if (interval == null) return;
9+
if (typeof interval === "number") {
10+
const n = interval;
11+
// Note: this offset doesn’t support the optional step argument for simplicity.
12+
interval = {floor: d => n * Math.floor(d / n), offset: d => d + n};
13+
}
14+
if (typeof interval.floor !== "function" || typeof interval.offset !== "function") throw new Error("invalid interval");
15+
return interval;
16+
}
17+
18+
// The interval may be specified either as x: {value, interval} or as {x,
19+
// interval}. The former is used, for example, for Plot.rect.
20+
function maybeIntervalValue(value, {interval} = {}) {
21+
value = {...maybeValue(value)};
22+
value.interval = maybeInterval(value.interval === undefined ? interval : value.interval);
23+
return value;
24+
}
25+
26+
function maybeIntervalK(k, maybeInsetK, options = {}) {
27+
const {[k]: v, [`${k}1`]: v1, [`${k}2`]: v2} = options;
28+
const {value, interval} = maybeIntervalValue(v, options);
29+
if (interval == null) return options;
30+
let V1;
31+
const tv1 = data => V1 || (V1 = valueof(data, value).map(v => interval.floor(v)));
32+
const label = labelof(v);
33+
return maybeInsetK({
34+
...options,
35+
[k]: undefined,
36+
[`${k}1`]: v1 === undefined ? {transform: tv1, label} : v1,
37+
[`${k}2`]: v2 === undefined ? {transform: () => tv1().map(v => interval.offset(v)), label} : v2
38+
});
39+
}
40+
41+
export function maybeIntervalX(options) {
42+
return maybeIntervalK("x", maybeInsetX, options);
43+
}
44+
45+
export function maybeIntervalY(options = {}) {
46+
return maybeIntervalK("y", maybeInsetY, options);
47+
}

0 commit comments

Comments
 (0)