Skip to content

Commit 14ded29

Browse files
authored
coerce to date for temporal thresholds (#572)
* coerce to date for temporal thresholds * Update README * Update README
1 parent 15de9c2 commit 14ded29

File tree

6 files changed

+154
-8
lines changed

6 files changed

+154
-8
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,10 +1178,10 @@ The **thresholds** option may be specified as a named method or a variety of oth
11781178
* *sturges* - [Sturges’ formula](https://en.wikipedia.org/wiki/Histogram#Sturges.27_formula)
11791179
* a count (hint) representing the desired number of bins
11801180
* an array of *n* threshold values for *n* + 1 bins
1181-
* a time interval (for temporal binning)
1181+
* an interval or time interval (for temporal binning; see below)
11821182
* a function that returns an array, count, or time interval
11831183

1184-
If the **thresholds** option is specified as a function, it is passed three arguments: the array of input values, the domain minimum, and the domain maximum. If a number, [d3.ticks](https://github.com/d3/d3-array/blob/main/README.md#ticks) or [d3.utcTicks](https://github.com/d3/d3-time/blob/master/README.md#ticks) is used to choose suitable nice thresholds.
1184+
If the **thresholds** option is specified as a function, it is passed three arguments: the array of input values, the domain minimum, and the domain maximum. If a number, [d3.ticks](https://github.com/d3/d3-array/blob/main/README.md#ticks) or [d3.utcTicks](https://github.com/d3/d3-time/blob/master/README.md#ticks) is used to choose suitable nice thresholds. If an interval, it must expose an *interval*.floor(*value*), *interval*.ceil(*value*), and *interval*.range(*start*, *stop*) methods. If the interval is a time interval such as d3.utcDay, or if the thresholds are specified as an array of dates, then the binned values are implicitly coerced to dates. Time intervals are intervals that are also functions that return a Date instance when called with no arguments.
11851185

11861186
The bin transform supports grouping in addition to binning: you can subdivide bins by up to two additional ordinal or categorical dimensions (not including faceting). If any of **z**, **fill**, or **stroke** is a channel, the first of these channels will be used to subdivide bins. Similarly, Plot.binX will group on **y** if **y** is not an output channel, and Plot.binY will group on **x** if **x** is not an output channel. For example, for a stacked histogram:
11871187

src/scales.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ function coerceArray(array, coerce, type = Array) {
286286
// Unlike Mark’s number, here we want to convert null and undefined to NaN,
287287
// since the result will be stored in a Float64Array and we don’t want null to
288288
// be coerced to zero.
289-
function coerceNumber(x) {
289+
export function coerceNumber(x) {
290290
return x == null ? NaN : +x;
291291
}
292292

@@ -296,7 +296,7 @@ function coerceNumber(x) {
296296
// it is still generally preferable to do date parsing yourself explicitly,
297297
// rather than rely on Plot.) Any non-string values are coerced to number first
298298
// and treated as milliseconds since UNIX epoch.
299-
function coerceDate(x) {
299+
export function coerceDate(x) {
300300
return x instanceof Date && !isNaN(x) ? x
301301
: typeof x === "string" ? isoParse(x)
302302
: x == null || isNaN(x = +x) ? undefined

src/transforms/bin.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
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 {coerceDate} from "../scales.js";
34
import {basic} from "./basic.js";
45
import {hasOutput, maybeEvaluator, maybeGroup, maybeOutput, maybeOutputs, maybeReduce, maybeSort, maybeSubgroup, reduceCount, reduceIdentity} from "./group.js";
56
import {maybeInsetX, maybeInsetY} from "./inset.js";
@@ -177,13 +178,14 @@ function maybeBin(options) {
177178
if (options == null) return;
178179
const {value, cumulative, domain = extent, thresholds} = options;
179180
const bin = data => {
180-
const V = valueof(data, value);
181+
let V = valueof(data, value);
181182
const bin = binner().value(i => V[i]);
182-
if (isTemporal(V)) {
183+
if (isTemporal(V) || isTimeThresholds(thresholds)) {
184+
V = V.map(coerceDate);
183185
let [min, max] = typeof domain === "function" ? domain(V) : domain;
184-
let t = typeof thresholds === "function" && !isTimeInterval(thresholds) ? thresholds(V, min, max) : thresholds;
186+
let t = typeof thresholds === "function" && !isInterval(thresholds) ? thresholds(V, min, max) : thresholds;
185187
if (typeof t === "number") t = utcTickInterval(min, max, t);
186-
if (isTimeInterval(t)) {
188+
if (isInterval(t)) {
187189
if (domain === extent) {
188190
min = t.floor(min);
189191
max = t.ceil(new Date(+max + 1));
@@ -219,7 +221,15 @@ function thresholdAuto(values, min, max) {
219221
return Math.min(200, thresholdScott(values, min, max));
220222
}
221223

224+
function isTimeThresholds(t) {
225+
return isTimeInterval(t) || t && t[Symbol.iterator] && isTemporal(t);
226+
}
227+
222228
function isTimeInterval(t) {
229+
return isInterval(t) && typeof t === "function" && t() instanceof Date;
230+
}
231+
232+
function isInterval(t) {
223233
return t ? typeof t.range === "function" : false;
224234
}
225235

test/output/untypedDateBin.svg

Lines changed: 120 additions & 0 deletions
Loading

test/plots/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export {default as stargazersHourlyGroup} from "./stargazers-hourly-group.js";
116116
export {default as stocksIndex} from "./stocks-index.js";
117117
export {default as travelersYearOverYear} from "./travelers-year-over-year.js";
118118
export {default as uniformRandomDifference} from "./uniform-random-difference.js";
119+
export {default as untypedDateBin} from "./untyped-date-bin.js";
119120
export {default as usCongressAge} from "./us-congress-age.js";
120121
export {default as usCongressAgeGender} from "./us-congress-age-gender.js";
121122
export {default as usPopulationStateAge} from "./us-population-state-age.js";

test/plots/untyped-date-bin.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as Plot from "@observablehq/plot";
2+
import * as d3 from "d3";
3+
4+
export default async function() {
5+
const aapl = await d3.csv("data/aapl.csv");
6+
return Plot.plot({
7+
y: {
8+
transform: d => d / 1e6
9+
},
10+
marks: [
11+
Plot.rectY(aapl, Plot.binX({y: "sum"}, {x: "Date", thresholds: d3.utcMonth, y: "Volume"})),
12+
Plot.ruleY([0])
13+
]
14+
});
15+
}

0 commit comments

Comments
 (0)