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 5 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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Observable Plot - Changelog

## 0.3.0

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

### Marks

The rect marks now accept an *interval* option that allows to derive *x1* and *x2* from *x*. A typical use case is an interval: d3.utcDay which creates a rect spanning the whole day that contains a certain date-time. The interval can be specified as an object with *floor** method that returns *x1* from *x* and an **offset** method that returns *x2* from *x1*. If the interval is specified as a (non-null) number, *x1* and *x2* are taken as the two consecutive multiples of *n* that bracket *x*.

## 0.2.1

Released September 19, 2021.
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