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

interval for rect #550

Merged
merged 6 commits into from
Sep 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
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
30 changes: 19 additions & 11 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
# Observable Plot - Changelog

## 0.2.1
## 0.2.3

*Not yet released.* These notes are a work in progress.

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*.

## 0.2.2

Released September 19, 2021.

### Marks
Fix a crash with the axis.tickRotate option when there are no ticks to rotate.

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.
## 0.2.1

### Scales
Released September 19, 2021.

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).
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.

### Transforms
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).

#### Plot.bin
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:

The reducers now receive the extent of the current bin as an argument after the data. For example, it allows to create meaningful titles:
```js
Plot.rect(
athletes,
Plot.bin(
{
fill: "count",
title: (bin, { x1, x2, y1, y2 }) =>
`${bin.length} athletes weighing between ${x1} and ${x2} and with a height between ${y1} and ${y2}`
title: (bin, {x1, x2, y1, y2}) => `${bin.length} athletes weighing between ${x1} and ${x2} and with a height between ${y1} and ${y2}`
},
{ x: "weight", y: "height", inset: 0 }
{
x: "weight",
y: "height",
inset: 0
}
)
).plot()
```
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,9 @@ The following channels are optional:
* **x2** - the ending horizontal position; bound to the *x* scale
* **y2** - the ending vertical position; bound to the *y* scale

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.
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.

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.

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

Expand Down
5 changes: 3 additions & 2 deletions src/marks/bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {filter} from "../defined.js";
import {Mark, number} from "../mark.js";
import {isCollapsed} from "../scales.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js";
import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js";
import {maybeStackX, maybeStackY} from "../transforms/stack.js";

const defaults = {};
Expand Down Expand Up @@ -116,9 +117,9 @@ export class BarY extends AbstractBar {
}

export function barX(data, options) {
return new BarX(data, maybeStackX(options));
return new BarX(data, maybeStackX(maybeIntervalX(options)));
}

export function barY(data, options) {
return new BarY(data, maybeStackY(options));
return new BarY(data, maybeStackY(maybeIntervalY(options)));
}
7 changes: 4 additions & 3 deletions src/marks/rect.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {filter} from "../defined.js";
import {Mark, number} from "../mark.js";
import {isCollapsed} from "../scales.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js";
import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js";
import {maybeStackX, maybeStackY} from "../transforms/stack.js";

const defaults = {};
Expand Down Expand Up @@ -64,13 +65,13 @@ export class Rect extends Mark {
}

export function rect(data, options) {
return new Rect(data, options);
return new Rect(data, maybeIntervalX(maybeIntervalY(options)));
}

export function rectX(data, options) {
return new Rect(data, maybeStackX(options));
return new Rect(data, maybeStackX(maybeIntervalY(options)));
}

export function rectY(data, options) {
return new Rect(data, maybeStackY(options));
return new Rect(data, maybeStackY(maybeIntervalX(options)));
}
11 changes: 7 additions & 4 deletions src/marks/rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {filter} from "../defined.js";
import {Mark, identity, number} from "../mark.js";
import {isCollapsed} from "../scales.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyChannelStyles, offset} from "../style.js";
import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js";

const defaults = {
fill: null,
Expand Down Expand Up @@ -97,14 +98,16 @@ export class RuleY extends Mark {
}
}

export function ruleX(data, {x = identity, y, y1, y2, ...options} = {}) {
export function ruleX(data, options) {
let {x = identity, y, y1, y2, ...rest} = maybeIntervalY(options);
([y1, y2] = maybeOptionalZero(y, y1, y2));
return new RuleX(data, {...options, x, y1, y2});
return new RuleX(data, {...rest, x, y1, y2});
}

export function ruleY(data, {y = identity, x, x1, x2, ...options} = {}) {
export function ruleY(data, options) {
let {y = identity, x, x1, x2, ...rest} = maybeIntervalX(options);
([x1, x2] = maybeOptionalZero(x, x1, x2));
return new RuleY(data, {...options, y, x1, x2});
return new RuleY(data, {...rest, y, x1, x2});
}

// For marks specified either as [0, x] or [x1, x2], or nothing.
Expand Down
30 changes: 9 additions & 21 deletions src/transforms/bin.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
import {bin as binner, extent, thresholdFreedmanDiaconis, thresholdScott, thresholdSturges, utcTickInterval} from "d3";
import {valueof, range, identity, maybeLazyChannel, maybeTuple, maybeColor, maybeValue, mid, labelof, isTemporal} from "../mark.js";
import {offset} from "../style.js";
import {basic} from "./basic.js";
import {maybeEvaluator, maybeGroup, maybeOutput, maybeOutputs, maybeReduce, maybeSort, maybeSubgroup, reduceCount, reduceIdentity} from "./group.js";
import {maybeInsetX, maybeInsetY} from "./inset.js";

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

// Group on {z, fill, stroke}, then optionally on x, then bin y.
export function binY(outputs = {x: "count"}, {inset, insetTop, insetBottom, ...options} = {}) {
let {x, y} = options;
y = maybeBinValue(y, options, identity);
([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom));
return binn(null, y, x, null, outputs, {inset, insetTop, insetBottom, ...options});
export function binY(outputs = {x: "count"}, options = {}) {
const {x, y} = options;
return binn(null, maybeBinValue(y, options, identity), x, null, outputs, maybeInsetY(options));
}

// Group on {z, fill, stroke}, then bin on x and y.
export function bin(outputs = {fill: "count"}, {inset, insetTop, insetRight, insetBottom, insetLeft, ...options} = {}) {
export function bin(outputs = {fill: "count"}, options = {}) {
const {x, y} = maybeBinValueTuple(options);
([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom));
([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight));
return binn(x, y, null, null, outputs, {inset, insetTop, insetRight, insetBottom, insetLeft, ...options});
return binn(x, y, null, null, outputs, maybeInsetX(maybeInsetY(options)));
}

function binn(
Expand Down Expand Up @@ -252,9 +246,3 @@ function binfilter([{x0, x1}, set]) {
function binempty() {
return new Uint32Array(0);
}

function maybeInset(inset, inset1, inset2) {
return inset === undefined && inset1 === undefined && inset2 === undefined
? (offset ? [1, 0] : [0.5, 0.5])
: [inset1, inset2];
}
17 changes: 17 additions & 0 deletions src/transforms/inset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {offset} from "../style.js";

export function maybeInsetX({inset, insetLeft, insetRight, ...options} = {}) {
([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight));
return {inset, insetLeft, insetRight, ...options};
}

export function maybeInsetY({inset, insetTop, insetBottom, ...options} = {}) {
([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom));
return {inset, insetTop, insetBottom, ...options};
}

function maybeInset(inset, inset1, inset2) {
return inset === undefined && inset1 === undefined && inset2 === undefined
? (offset ? [1, 0] : [0.5, 0.5])
Copy link
Contributor

Choose a reason for hiding this comment

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

(This is not new) The default inset makes the marks vanish if they are too narrow. For example in aapl-volume-rect, if you take the whole dataset instead of just a slice, the resulting chart is blank.

Copy link
Contributor

Choose a reason for hiding this comment

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

refs. #422 and d3/d3-scale#243

Copy link
Member Author

Choose a reason for hiding this comment

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

I’m aware of this problem but not going to fix it here. 👍

: [inset1, inset2];
}
47 changes: 47 additions & 0 deletions src/transforms/interval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {labelof, maybeValue, valueof} from "../mark.js";
import {maybeInsetX, maybeInsetY} from "./inset.js";

// TODO Allow the interval to be specified as a string, e.g. “day” or “hour”?
// This will require the interval knowing the type of the associated scale to
// chose between UTC and local time (or better, an explicit timeZone option).
function maybeInterval(interval) {
if (interval == null) return;
if (typeof interval === "number") {
const n = interval;
// Note: this offset doesn’t support the optional step argument for simplicity.
interval = {floor: d => n * Math.floor(d / n), offset: d => d + n};
}
if (typeof interval.floor !== "function" || typeof interval.offset !== "function") throw new Error("invalid interval");
return interval;
}

// The interval may be specified either as x: {value, interval} or as {x,
// interval}. The former is used, for example, for Plot.rect.
function maybeIntervalValue(value, {interval} = {}) {
value = {...maybeValue(value)};
value.interval = maybeInterval(value.interval === undefined ? interval : value.interval);
return value;
}

function maybeIntervalK(k, maybeInsetK, options = {}) {
const {[k]: v, [`${k}1`]: v1, [`${k}2`]: v2} = options;
const {value, interval} = maybeIntervalValue(v, options);
if (interval == null) return options;
let V1;
const tv1 = data => V1 || (V1 = valueof(data, value).map(v => interval.floor(v)));
const label = labelof(v);
return maybeInsetK({
...options,
[k]: undefined,
[`${k}1`]: v1 === undefined ? {transform: tv1, label} : v1,
[`${k}2`]: v2 === undefined ? {transform: () => tv1().map(v => interval.offset(v)), label} : v2
});
}

export function maybeIntervalX(options) {
return maybeIntervalK("x", maybeInsetX, options);
}

export function maybeIntervalY(options = {}) {
return maybeIntervalK("y", maybeInsetY, options);
}
Loading