-
Notifications
You must be signed in to change notification settings - Fork 189
time intervals coerce isostrings #1326
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
Changes from all commits
55576ca
065ed2b
0a57694
24233ef
c03dccb
7e202e3
5cf3fea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,18 @@ import {createLegends, exposeLegends} from "./legends.js"; | |
import {Mark} from "./mark.js"; | ||
import {axisFx, axisFy, axisX, axisY, gridFx, gridFy, gridX, gridY} from "./marks/axis.js"; | ||
import {frame} from "./marks/frame.js"; | ||
import {arrayify, isColor, isIterable, isNone, isScaleOptions, map, yes, maybeInterval} from "./options.js"; | ||
import { | ||
arrayify, | ||
coerceDate, | ||
isColor, | ||
isIterable, | ||
isNone, | ||
isScaleOptions, | ||
isTimeInterval, | ||
map, | ||
yes, | ||
maybeInterval | ||
} from "./options.js"; | ||
import {createScales, createScaleFunctions, autoScaleRange, exposeScales} from "./scales.js"; | ||
import {innerDimensions, outerDimensions} from "./scales.js"; | ||
import {position, registry as scaleRegistry} from "./scales/index.js"; | ||
|
@@ -358,12 +369,13 @@ function applyScaleTransforms(channels, options) { | |
function applyScaleTransform(channel, options) { | ||
const {scale} = channel; | ||
if (scale == null) return; | ||
const { | ||
type, | ||
percent, | ||
interval, | ||
transform = percent ? (x) => x * 100 : maybeInterval(interval, type)?.floor | ||
} = options[scale] ?? {}; | ||
let {type, percent, interval, transform} = options[scale] ?? {}; | ||
if (transform === undefined) { | ||
if (percent) return (channel.value = channel.value.map((x) => x * 100)), undefined; | ||
const i = maybeInterval(interval, type); | ||
if (isTimeInterval(i)) return (channel.value = channel.value.map((d) => i.floor(coerceDate(d)))), undefined; | ||
transform = i?.floor; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be cleaner to refactor this into a helper like so: function maybeIntervalTransform(interval, type) {
interval = maybeInterval(interval, type);
return isTimeInterval(interval) ? (d) => interval.floor(coerceDate(d)) : interval?.floor;
} And then replace the default like so: transform = percent ? (x) => x * 100 : maybeIntervalTransform(interval, type) In other words, I think it’s preferable to consolidate the code paths so that the interval is still promoted to a transform. And also we’d want to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also do we want to coerce to numbers for number intervals? Should we further assume that intervals only apply to numbers and dates and not other types? 🤔 |
||
if (transform != null) channel.value = map(channel.value, transform); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,3 +1,4 @@ | ||||||
import {coerceDate, isTimeInterval} from "../options.js"; | ||||||
import {isTemporal, labelof, map, maybeInterval, maybeValue, valueof} from "../options.js"; | ||||||
import {maybeInsetX, maybeInsetY} from "./inset.js"; | ||||||
|
||||||
|
@@ -28,7 +29,9 @@ function maybeIntervalK(k, maybeInsetK, options, trivial) { | |||||
let D1, V1; | ||||||
function transform(data) { | ||||||
if (V1 !== undefined && data === D1) return V1; // memoize | ||||||
return (V1 = map(valueof((D1 = data), value), (v) => interval.floor(v))); | ||||||
let V = valueof((D1 = data), value); | ||||||
if (isTimeInterval(interval)) V = map(V, coerceDate, Float64Array); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think we want the slower Also, given that we can’t use a typed array here, it will probably be faster to fold the |
||||||
return (V1 = map(V, (v) => interval.floor(v))); | ||||||
} | ||||||
return maybeInsetK({ | ||||||
...options, | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import * as Plot from "@observablehq/plot"; | ||
import * as d3 from "d3"; | ||
|
||
export async function dateIntervalWeekMonth() { | ||
const data = await d3.csv("data/bls-industry-unemployment.csv"); | ||
return Plot.plot({ | ||
marginLeft: 80, | ||
color: {type: "linear", scheme: "blues"}, | ||
marks: ( | ||
[ | ||
{label: "month", interval: "month"}, | ||
{label: "d3.utcMonth", interval: d3.utcMonth}, | ||
{label: "week", interval: "week"}, | ||
{label: "d3.utcWeek", interval: d3.utcWeek} | ||
] as const | ||
).map(({label, interval}) => | ||
Plot.barX(data, { | ||
x: "date", | ||
filter: (d) => d.industry === "Construction", | ||
interval, | ||
fill: "unemployed", | ||
title: "unemployed", | ||
inset: 0, | ||
fy: () => label | ||
}) | ||
) | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This disagrees with how we’ve defined intervals in TypeScript, where they are objects rather than decorated functions:
plot/src/interval.d.ts
Lines 19 to 22 in 820144e
And this means that you can use a {floor, offset, range?} object in Plot, which works, except when we try to do this time interval test.
Also, I don’t think we actually need this restriction, because D3 doesn’t require that time intervals are functions, either. It instead checks for the range method (and then blindly invokes the floor and offset methods):
https://github.com/d3/d3-time/blob/9e8dc940f38f78d7588aad68a54a25b1f0c2d97b/src/ticks.js#L38
https://github.com/d3/d3-scale/blob/2b7db622b1a224d9ea19ff15c4cc8cbb3b25f4a4/src/time.js#L58
I added that duck testing in #572, but I think it would be better to replace that duck testing with something consistent with our new TypeScript declarations and with D3. Probably we should invoke the floor method and see if it returns a Date? But that’s a little messy too, since that method requires an argument, and it’s unclear whether we should pass it a Date or a number… All D3 time intervals support coercion (as does Plot’s internal number interval), but should we expect users to implement coercion in their own intervals? I mean, isn’t the point of this PR is that they don’t? 🤔