Skip to content

Commit 0c963df

Browse files
authored
strict range interval, nice interval (#1332)
* strict range interval * nice intervals
1 parent bbacf75 commit 0c963df

File tree

10 files changed

+52
-42
lines changed

10 files changed

+52
-42
lines changed

src/interval.d.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,21 @@ export type TimeIntervalName =
1818

1919
export interface IntervalImplementation<T> {
2020
floor(value: T): T;
21-
offset(value: T, offset: number): T;
21+
offset(value: T, offset?: number): T;
2222
}
2323

2424
export interface RangeIntervalImplementation<T> extends IntervalImplementation<T> {
2525
range(start: T, stop: T): T[];
2626
}
2727

28-
export type TimeInterval = TimeIntervalName | IntervalImplementation<Date>;
29-
30-
export type TimeRangeInterval = TimeIntervalName | RangeIntervalImplementation<Date>;
28+
export interface NiceIntervalImplementation<T> extends RangeIntervalImplementation<T> {
29+
ceil(value: T): T;
30+
}
3131

32-
export type NumberInterval = number | IntervalImplementation<number>;
32+
type LiteralInterval<T> = T extends Date ? TimeIntervalName : T extends number ? number : never;
3333

34-
export type NumberRangeInterval = number | RangeIntervalImplementation<number>;
34+
export type Interval<T = any> = LiteralInterval<T> | IntervalImplementation<T>;
3535

36-
export type Interval = TimeInterval | NumberInterval;
36+
export type RangeInterval<T = any> = LiteralInterval<T> | RangeIntervalImplementation<T>;
3737

38-
export type RangeInterval = TimeRangeInterval | NumberRangeInterval;
38+
export type NiceInterval<T = any> = LiteralInterval<T> | NiceIntervalImplementation<T>;

src/marks/axis.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ export interface AxisXOptions extends AxisOptions, TickXOptions {}
3333

3434
export interface AxisYOptions extends AxisOptions, TickYOptions {}
3535

36-
export interface GridXOptions extends GridOptions, RuleXOptions {}
36+
export interface GridXOptions extends GridOptions, Omit<RuleXOptions, "interval"> {}
3737

38-
export interface GridYOptions extends GridOptions, RuleYOptions {}
38+
export interface GridYOptions extends GridOptions, Omit<RuleYOptions, "interval"> {}
3939

4040
export function axisY(options?: AxisYOptions): CompoundMark;
4141

src/marks/axis.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {formatDefault} from "../format.js";
33
import {marks} from "../mark.js";
44
import {radians} from "../math.js";
55
import {range, valueof, arrayify, constant, keyword, identity, number} from "../options.js";
6-
import {isNoneish, isIterable, isTemporal, maybeInterval, orderof} from "../options.js";
6+
import {isNoneish, isIterable, isTemporal, maybeRangeInterval, orderof} from "../options.js";
77
import {isTemporalScale} from "../scales.js";
88
import {offset} from "../style.js";
99
import {initializer} from "../transforms/basic.js";
@@ -511,7 +511,7 @@ function axisMark(mark, k, ariaLabel, data, options, initialize) {
511511
if (ticks !== undefined) {
512512
data = scale.ticks(ticks);
513513
} else {
514-
interval = maybeInterval(interval === undefined ? scale.interval : interval, scale.type); // TODO check for RangeInterval
514+
interval = maybeRangeInterval(interval === undefined ? scale.interval : interval, scale.type);
515515
if (interval !== undefined) {
516516
// For time scales, we could pass the interval directly to
517517
// scale.ticks because it’s supported by d3.utcTicks; but

src/options.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,20 @@ export function maybeInterval(interval, type) {
288288
return interval;
289289
}
290290

291+
// Like maybeInterval, but requires a range method too.
292+
export function maybeRangeInterval(interval, type) {
293+
interval = maybeInterval(interval, type);
294+
if (interval && typeof interval.range !== "function") throw new Error("invalid interval: missing range method");
295+
return interval;
296+
}
297+
298+
// Like maybeRangeInterval, but requires a ceil method too.
299+
export function maybeNiceInterval(interval, type) {
300+
interval = maybeRangeInterval(interval, type);
301+
if (interval && typeof interval.ceil !== "function") throw new Error("invalid interval: missing ceil method");
302+
return interval;
303+
}
304+
291305
// This distinguishes between per-dimension options and a standalone value.
292306
export function maybeValue(value) {
293307
return value === undefined || isOptions(value) ? value : {value};

src/scales.d.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type {ColorSchemeName} from "./color.js";
22
import type {InsetOptions} from "./inset.js";
33
import type {Interpolate} from "./interpolate.js";
4-
import type {Interval} from "./interval.js";
4+
import type {NiceInterval, RangeInterval} from "./interval.js";
55
import type {LegendType} from "./legends.js";
66
import type {AxisAnchor} from "./marks/axis.js";
77

@@ -34,15 +34,15 @@ export type ScaleType =
3434

3535
export interface ScaleDefaults extends InsetOptions {
3636
clamp?: boolean;
37-
nice?: boolean | number | Interval;
37+
nice?: boolean | number | NiceInterval;
3838
zero?: boolean;
3939
round?: boolean;
4040
align?: number;
4141
padding?: number;
4242

4343
// axis options
4444
axis?: AxisAnchor | "both" | boolean | null; // for position scales
45-
grid?: boolean | string | Interval | Iterable<any>;
45+
grid?: boolean | string | RangeInterval | Iterable<any>;
4646
label?: string | null;
4747
}
4848

@@ -101,7 +101,7 @@ export interface ScaleOptions extends ScaleDefaults {
101101
transform?: (t: any) => any;
102102

103103
// quantitative scale options
104-
interval?: Interval; // TODO RangeInterval?
104+
interval?: RangeInterval;
105105
percent?: boolean;
106106

107107
// color scale options
@@ -131,7 +131,7 @@ export interface ScaleOptions extends ScaleDefaults {
131131

132132
// axis and legend options
133133
legend?: LegendType | boolean | null; // for color, opacity, and symbol scales
134-
ticks?: number | Interval | Iterable<any>; // TODO RangeInterval?
134+
ticks?: number | RangeInterval | Iterable<any>;
135135
tickSize?: number;
136136
tickSpacing?: number;
137137
tickPadding?: number;
@@ -153,7 +153,7 @@ export interface Scale {
153153
transform?: (t: any) => any;
154154
percent?: boolean;
155155
unknown?: any;
156-
interval?: Interval; // TODO RangeInterval?
156+
interval?: RangeInterval;
157157
interpolate?: Interpolate;
158158
clamp?: boolean;
159159
pivot?: any;

src/scales/ordinal.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {InternSet, extent, quantize, reverse as reverseof, sort, symbolsFill, symbolsStroke} from "d3";
22
import {scaleBand, scaleOrdinal, scalePoint, scaleImplicit} from "d3";
33
import {ascendingDefined} from "../defined.js";
4-
import {isNoneish, map, maybeInterval} from "../options.js";
4+
import {isNoneish, map, maybeRangeInterval} from "../options.js";
55
import {maybeSymbol} from "../symbol.js";
66
import {registry, color, position, symbol} from "./index.js";
77
import {maybeBooleanRange, ordinalScheme, quantitativeScheme} from "./schemes.js";
@@ -13,7 +13,7 @@ import {maybeBooleanRange, ordinalScheme, quantitativeScheme} from "./schemes.js
1313
export const ordinalImplicit = Symbol("ordinal");
1414

1515
function createScaleO(key, scale, channels, {type, interval, domain, range, reverse, hint}) {
16-
interval = maybeInterval(interval, type);
16+
interval = maybeRangeInterval(interval, type);
1717
if (domain === undefined) domain = inferDomain(channels, interval, key);
1818
if (type === "categorical" || type === ordinalImplicit) type = "ordinal"; // shorthand for color schemes
1919
if (reverse) domain = reverseof(domain);
@@ -27,7 +27,7 @@ function createScaleO(key, scale, channels, {type, interval, domain, range, reve
2727
}
2828

2929
export function createScaleOrdinal(key, channels, {type, interval, domain, range, scheme, unknown, ...options}) {
30-
interval = maybeInterval(interval, type);
30+
interval = maybeRangeInterval(interval, type);
3131
if (domain === undefined) domain = inferDomain(channels, interval, key);
3232
let hint;
3333
if (registry.get(key) === symbol) {

src/scales/quantitative.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
ticks
2424
} from "d3";
2525
import {positive, negative, finite} from "../defined.js";
26-
import {arrayify, constant, orderof, slice, maybeInterval} from "../options.js";
26+
import {arrayify, constant, orderof, slice, maybeNiceInterval, maybeRangeInterval} from "../options.js";
2727
import {ordinalRange, quantitativeScheme} from "./schemes.js";
2828
import {registry, radius, opacity, color, length} from "./index.js";
2929

@@ -78,7 +78,7 @@ export function createScaleQ(
7878
reverse
7979
}
8080
) {
81-
interval = maybeInterval(interval, type);
81+
interval = maybeRangeInterval(interval, type);
8282
if (type === "cyclical" || type === "sequential") type = "linear"; // shorthand for color schemes
8383
reverse = !!reverse;
8484

@@ -121,12 +121,16 @@ export function createScaleQ(
121121

122122
if (reverse) domain = reverseof(domain);
123123
scale.domain(domain).unknown(unknown);
124-
if (nice) scale.nice(nice === true ? undefined : nice), (domain = scale.domain());
124+
if (nice) scale.nice(maybeNice(nice, type)), (domain = scale.domain());
125125
if (range !== undefined) scale.range(range);
126126
if (clamp) scale.clamp(clamp);
127127
return {type, domain, range, scale, interpolate, interval};
128128
}
129129

130+
function maybeNice(nice, type) {
131+
return nice === true ? undefined : typeof nice === "number" ? nice : maybeNiceInterval(nice, type);
132+
}
133+
130134
export function createScaleLinear(key, channels, options) {
131135
return createScaleQ(key, scaleLinear(), channels, options);
132136
}

src/transforms/bin.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type {ChannelReducers} from "../channel.js";
2-
import type {Interval, RangeInterval} from "../interval.js";
2+
import type {RangeInterval} from "../interval.js";
33
import type {Reducer} from "../reducer.js";
44
import type {Transformed} from "./basic.js";
55

66
export type ThresholdsName = "freedman-diaconis" | "scott" | "sturges" | "auto";
77

88
export type ThresholdsFunction = (values: any[], min: any, max: any) => any[];
99

10-
export type Thresholds = ThresholdsName | ThresholdsFunction | Interval;
10+
export type Thresholds = ThresholdsName | ThresholdsFunction | RangeInterval;
1111

1212
export interface BinOptions {
1313
cumulative?: boolean | number;

src/transforms/bin.js

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
coerceDate,
1515
coerceNumbers,
1616
maybeColumn,
17-
maybeInterval,
17+
maybeRangeInterval,
1818
maybeTuple,
1919
maybeColorChannel,
2020
maybeValue,
@@ -24,6 +24,7 @@ import {
2424
isIterable,
2525
map
2626
} from "../options.js";
27+
import {maybeUtcInterval} from "../time.js";
2728
import {basic} from "./basic.js";
2829
import {
2930
hasOutput,
@@ -311,20 +312,11 @@ export function maybeThresholds(thresholds, interval, defaultThresholds = thresh
311312
case "auto":
312313
return thresholdAuto;
313314
}
314-
const interval = maybeInterval(thresholds);
315-
if (interval !== undefined) return interval;
316-
throw new Error(`invalid thresholds: ${thresholds}`);
315+
return maybeUtcInterval(thresholds);
317316
}
318317
return thresholds; // pass array, count, or function to bin.thresholds
319318
}
320319

321-
// Unlike the interval transform, we require a range method, too.
322-
function maybeRangeInterval(interval) {
323-
interval = maybeInterval(interval);
324-
if (!isInterval(interval)) throw new Error(`invalid interval: ${interval}`);
325-
return interval;
326-
}
327-
328320
function thresholdAuto(values, min, max) {
329321
return Math.min(200, thresholdScott(values, min, max));
330322
}
@@ -338,7 +330,7 @@ function isTimeInterval(t) {
338330
}
339331

340332
function isInterval(t) {
341-
return t ? typeof t.range === "function" : false;
333+
return typeof t?.range === "function";
342334
}
343335

344336
function bing(EX, EY) {

test/plots/electricity-demand.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ export async function electricityDemand() {
66
return Plot.plot({
77
width: 960,
88
marginLeft: 50,
9-
x: {round: true, nice: d3.utcWeek},
9+
x: {round: true, nice: "week"},
1010
y: {insetTop: 6},
1111
marks: [
1212
Plot.frame({fill: "#efefef"}),
1313
Plot.ruleY([0]),
14-
Plot.axisX({ticks: d3.utcYear, tickSize: 28, tickPadding: -11, tickFormat: " %Y", textAnchor: "start"}),
15-
Plot.axisX({ticks: d3.utcMonth, tickSize: 16, tickPadding: -11, tickFormat: " %B", textAnchor: "start"}),
16-
Plot.gridX({ticks: d3.utcWeek, stroke: "#fff", strokeOpacity: 1, insetBottom: -0.5}),
14+
Plot.axisX({ticks: "year", tickSize: 28, tickPadding: -11, tickFormat: " %Y", textAnchor: "start"}),
15+
Plot.axisX({ticks: "month", tickSize: 16, tickPadding: -11, tickFormat: " %B", textAnchor: "start"}),
16+
Plot.gridX({ticks: "week", stroke: "#fff", strokeOpacity: 1, insetBottom: -0.5}),
1717
Plot.dot(electricity, {x: "date", y: "mwh", stroke: "red", strokeOpacity: 0.3}),
1818
Plot.line(electricity, Plot.windowY(24, {x: "date", y: "mwh"}))
1919
]

0 commit comments

Comments
 (0)