Skip to content

Commit 640e3f9

Browse files
tophtuckermbostock
andauthored
Auto mark: never render rect; move zero-ness to autoSpec (#1368)
* Auto mark: never render rect; move zero-ness to autoSpec * update test artifacts * fix some tests; only set zero on a dimension if that dimension is defined * Update src/marks/auto.js Co-authored-by: Mike Bostock <mbostock@gmail.com> * dont set zero-ness if colorReduce * fix some tests * prettier * just committing the state after pairing so i have it * revert auto file * re-fix the original motivating bugs, i think * autoImpl * rm autoplot matrix test bc it didnt work with test runner * transformImpl; coerce zero; sort imports; const * normalize mark option --------- Co-authored-by: Mike Bostock <mbostock@gmail.com>
1 parent a436fbd commit 640e3f9

File tree

8 files changed

+515
-301
lines changed

8 files changed

+515
-301
lines changed

src/marks/auto.js

Lines changed: 132 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,35 @@
11
import {ascending, InternSet} from "d3";
2-
import {isOrdinal, labelof, valueof, isOptions, isColor, isObject} from "../options.js";
2+
import {marks} from "../mark.js";
3+
import {isColor, isObject, isOptions, isOrdinal, labelof, valueof} from "../options.js";
4+
import {bin, binX, binY} from "../transforms/bin.js";
5+
import {group, groupX, groupY} from "../transforms/group.js";
36
import {areaX, areaY} from "./area.js";
4-
import {dot} from "./dot.js";
5-
import {line, lineX, lineY} from "./line.js";
6-
import {ruleX, ruleY} from "./rule.js";
77
import {barX, barY} from "./bar.js";
8-
import {rect, rectX, rectY} from "./rect.js";
98
import {cell} from "./cell.js";
9+
import {dot} from "./dot.js";
1010
import {frame} from "./frame.js";
11-
import {bin, binX, binY} from "../transforms/bin.js";
12-
import {group, groupX, groupY} from "../transforms/group.js";
13-
import {marks} from "../mark.js";
11+
import {line, lineX, lineY} from "./line.js";
12+
import {rectX, rectY} from "./rect.js";
13+
import {ruleX, ruleY} from "./rule.js";
1414

1515
export function autoSpec(data, options) {
16+
const {x, y, fx, fy, color, size, mark} = autoImpl(data, options);
17+
return {x, y, fx, fy, color, size, mark};
18+
}
19+
20+
function autoImpl(data, options) {
1621
options = normalizeOptions(options);
1722

23+
// Greedily materialize columns for type inference; we’ll need them anyway to
24+
// plot! Note that we don’t apply any type inference to the fx and fy
25+
// channels, if present; these are always ordinal (at least for now).
26+
const {x, y, color, size} = options;
27+
const X = materializeValue(data, x);
28+
const Y = materializeValue(data, y);
29+
const C = materializeValue(data, color);
30+
const S = materializeValue(data, size);
31+
32+
// Compute the default options.
1833
let {
1934
fx,
2035
fy,
@@ -25,10 +40,6 @@ export function autoSpec(data, options) {
2540
mark
2641
} = options;
2742

28-
// Lazily materialize x and y columns for type inference, if needed.
29-
const {x, y} = options;
30-
let X, Y;
31-
3243
// Determine the default reducer, if any.
3344
if (xReduce === undefined)
3445
xReduce = yReduce == null && xValue == null && sizeValue == null && yValue != null ? "count" : null;
@@ -42,8 +53,8 @@ export function autoSpec(data, options) {
4253
colorReduce == null &&
4354
xReduce == null &&
4455
yReduce == null &&
45-
(xValue == null || isOrdinal((X ??= materializeValue(data, x)))) &&
46-
(yValue == null || isOrdinal((Y ??= materializeValue(data, y))))
56+
(xValue == null || isOrdinal(X)) &&
57+
(yValue == null || isOrdinal(Y))
4758
) {
4859
sizeReduce = "count";
4960
}
@@ -62,121 +73,61 @@ export function autoSpec(data, options) {
6273
mark =
6374
sizeValue != null || sizeReduce != null
6475
? "dot"
65-
: xZero || yZero || colorReduce != null // histogram or heatmap
76+
: isZeroReducer(xReduce) || isZeroReducer(yReduce) || colorReduce != null // histogram or heatmap
6677
? "bar"
6778
: xValue != null && yValue != null
68-
? isOrdinal((X ??= materializeValue(data, x))) ||
69-
isOrdinal((Y ??= materializeValue(data, y))) ||
70-
(xReduce == null && yReduce == null && !isMonotonic(X) && !isMonotonic(Y))
79+
? isOrdinal(X) || isOrdinal(Y) || (xReduce == null && yReduce == null && !isMonotonic(X) && !isMonotonic(Y))
7180
? "dot"
7281
: "line"
7382
: xValue != null || yValue != null
7483
? "rule"
7584
: null;
7685
}
7786

78-
return {
79-
fx: fx ?? null,
80-
fy: fy ?? null,
81-
x: {
82-
value: xValue ?? null,
83-
reduce: xReduce ?? null,
84-
...(xZero !== undefined && {zero: xZero}), // TODO realize default
85-
...xOptions
86-
},
87-
y: {
88-
value: yValue ?? null,
89-
reduce: yReduce ?? null,
90-
...(yZero !== undefined && {zero: yZero}), // TODO realize default
91-
...yOptions
92-
},
93-
color: {
94-
value: colorValue ?? null,
95-
reduce: colorReduce ?? null,
96-
...(colorColor !== undefined && {color: colorColor})
97-
},
98-
size: {
99-
value: sizeValue ?? null,
100-
reduce: sizeReduce ?? null
101-
},
102-
mark
103-
};
104-
}
105-
106-
export function auto(data, options) {
107-
options = normalizeOptions(options);
108-
109-
// Greedily materialize columns for type inference; we’ll need them anyway to
110-
// plot! Note that we don’t apply any type inference to the fx and fy
111-
// channels, if present; these are always ordinal (at least for now).
112-
const {x, y, color, size} = options;
113-
const X = materializeValue(data, x);
114-
const Y = materializeValue(data, y);
115-
const C = materializeValue(data, color);
116-
const S = materializeValue(data, size);
117-
118-
// Compute the default options via autoSpec.
119-
let {
120-
fx,
121-
fy,
122-
x: {reduce: xReduce, zero: xZero, ...xOptions},
123-
y: {reduce: yReduce, zero: yZero, ...yOptions},
124-
color: {color: colorColor, reduce: colorReduce},
125-
size: {reduce: sizeReduce},
126-
mark
127-
} = autoSpec(data, {
128-
...options,
129-
x: {...x, value: X},
130-
y: {...y, value: Y},
131-
color: {...color, value: C},
132-
size: {...size, value: S}
133-
});
134-
13587
let Z; // may be set to null to disable series-by-color for line and area
13688
let colorMode; // "fill" or "stroke"
13789

13890
// Determine the mark implementation.
139-
if (mark != null) {
140-
switch (`${mark}`.toLowerCase()) {
141-
case "dot":
142-
mark = dot;
143-
colorMode = "stroke";
144-
break;
145-
case "line":
146-
mark = X && Y ? line : X ? lineX : lineY; // 1d line by index
147-
colorMode = "stroke";
148-
if (isHighCardinality(C)) Z = null; // TODO only if z not set by user
149-
break;
150-
case "area":
151-
mark = yZero ? areaY : xZero || (Y && isMonotonic(Y)) ? areaX : areaY; // favor areaY if unsure
152-
colorMode = "fill";
153-
if (isHighCardinality(C)) Z = null; // TODO only if z not set by user
154-
break;
155-
case "rule":
156-
mark = X ? ruleX : ruleY;
157-
colorMode = "stroke";
158-
break;
159-
case "bar":
160-
mark = yZero
161-
? isOrdinalReduced(xReduce, X)
162-
? barY
163-
: rectY
164-
: xZero
165-
? isOrdinalReduced(yReduce, Y)
166-
? barX
167-
: rectX
168-
: isOrdinalReduced(xReduce, X) && isOrdinalReduced(yReduce, Y)
169-
? cell
170-
: isOrdinalReduced(xReduce, X)
91+
let markImpl;
92+
switch (mark) {
93+
case "dot":
94+
markImpl = dot;
95+
colorMode = "stroke";
96+
break;
97+
case "line":
98+
markImpl = X && Y ? line : X ? lineX : lineY; // 1d line by index
99+
colorMode = "stroke";
100+
if (isHighCardinality(C)) Z = null; // TODO only if z not set by user
101+
break;
102+
case "area":
103+
markImpl = yZero ? areaY : xZero || (Y && isMonotonic(Y)) ? areaX : areaY; // favor areaY if unsure
104+
colorMode = "fill";
105+
if (isHighCardinality(C)) Z = null; // TODO only if z not set by user
106+
break;
107+
case "rule":
108+
markImpl = X ? ruleX : ruleY;
109+
colorMode = "stroke";
110+
break;
111+
case "bar":
112+
markImpl = yZero
113+
? isOrdinalReduced(xReduce, X)
171114
? barY
172-
: isOrdinalReduced(yReduce, Y)
115+
: rectY
116+
: xZero
117+
? isOrdinalReduced(yReduce, Y)
173118
? barX
174-
: rect;
175-
colorMode = "fill";
176-
break;
177-
default:
178-
throw new Error(`invalid mark: ${mark}`);
179-
}
119+
: rectX
120+
: isOrdinalReduced(xReduce, X) && isOrdinalReduced(yReduce, Y)
121+
? cell
122+
: isOrdinalReduced(xReduce, X)
123+
? barY
124+
: isOrdinalReduced(yReduce, Y)
125+
? barX
126+
: rectY;
127+
colorMode = "fill";
128+
break;
129+
default:
130+
throw new Error(`invalid mark: ${mark}`);
180131
}
181132

182133
// Determine the mark options.
@@ -189,44 +140,95 @@ export function auto(data, options) {
189140
z: Z,
190141
r: S ?? undefined // treat null size as undefined for default constant radius
191142
};
192-
let transform;
143+
let transformImpl;
193144
let transformOptions = {[colorMode]: colorReduce ?? undefined, r: sizeReduce ?? undefined};
194145
if (xReduce != null && yReduce != null) {
195146
throw new Error(`cannot reduce both x and y`); // for now at least
196147
} else if (yReduce != null) {
197148
transformOptions.y = yReduce;
198-
transform = isOrdinal(X) ? groupX : binX;
149+
transformImpl = isOrdinal(X) ? groupX : binX;
199150
} else if (xReduce != null) {
200151
transformOptions.x = xReduce;
201-
transform = isOrdinal(Y) ? groupY : binY;
152+
transformImpl = isOrdinal(Y) ? groupY : binY;
202153
} else if (colorReduce != null || sizeReduce != null) {
203154
if (X && Y) {
204-
transform = isOrdinal(X) && isOrdinal(Y) ? group : isOrdinal(X) ? binY : isOrdinal(Y) ? binX : bin;
155+
transformImpl = isOrdinal(X) && isOrdinal(Y) ? group : isOrdinal(X) ? binY : isOrdinal(Y) ? binX : bin;
205156
} else if (X) {
206-
transform = isOrdinal(X) ? groupX : binX;
157+
transformImpl = isOrdinal(X) ? groupX : binX;
207158
} else if (Y) {
208-
transform = isOrdinal(Y) ? groupY : binY;
159+
transformImpl = isOrdinal(Y) ? groupY : binY;
209160
}
210161
}
211-
if (transform) {
212-
if (transform === bin || transform === binX) markOptions.x = {value: X, ...xOptions};
213-
if (transform === bin || transform === binY) markOptions.y = {value: Y, ...yOptions};
214-
markOptions = transform(transformOptions, markOptions);
215-
}
162+
163+
// When using the bin transform, pass through additional options (e.g., thresholds).
164+
if (transformImpl === bin || transformImpl === binX) markOptions.x = {value: X, ...xOptions};
165+
if (transformImpl === bin || transformImpl === binY) markOptions.y = {value: Y, ...yOptions};
216166

217167
// If zero-ness is not specified, default based on whether the resolved mark
218-
// type will include a zero baseline. TODO Move this to autoSpec.
168+
// type will include a zero baseline.
219169
if (xZero === undefined)
220-
xZero = X && transform !== binX && (mark === barX || mark === areaX || mark === rectX || mark === ruleY);
170+
xZero =
171+
X &&
172+
!(transformImpl === bin || transformImpl === binX) &&
173+
(markImpl === barX || markImpl === areaX || markImpl === rectX || markImpl === ruleY);
221174
if (yZero === undefined)
222-
yZero = Y && transform !== binY && (mark === barY || mark === areaY || mark === rectY || mark === ruleX);
175+
yZero =
176+
Y &&
177+
!(transformImpl === bin || transformImpl === binY) &&
178+
(markImpl === barY || markImpl === areaY || markImpl === rectY || markImpl === ruleX);
179+
180+
return {
181+
fx: fx ?? null,
182+
fy: fy ?? null,
183+
x: {
184+
value: xValue ?? null,
185+
reduce: xReduce ?? null,
186+
zero: !!xZero,
187+
...xOptions
188+
},
189+
y: {
190+
value: yValue ?? null,
191+
reduce: yReduce ?? null,
192+
zero: !!yZero,
193+
...yOptions
194+
},
195+
color: {
196+
value: colorValue ?? null,
197+
reduce: colorReduce ?? null,
198+
...(colorColor !== undefined && {color: colorColor})
199+
},
200+
size: {
201+
value: sizeValue ?? null,
202+
reduce: sizeReduce ?? null
203+
},
204+
mark,
205+
markImpl,
206+
markOptions,
207+
transformImpl,
208+
transformOptions,
209+
colorMode
210+
};
211+
}
212+
213+
export function auto(data, options) {
214+
const {
215+
fx,
216+
fy,
217+
x: {zero: xZero},
218+
y: {zero: yZero},
219+
markImpl,
220+
markOptions,
221+
transformImpl,
222+
transformOptions,
223+
colorMode
224+
} = autoImpl(data, options);
223225

224226
// In the case of filled marks (particularly bars and areas) the frame and
225227
// rules should come after the mark; in the case of stroked marks
226228
// (particularly dots and lines) they should come before the mark.
227229
const frames = fx != null || fy != null ? frame({strokeOpacity: 0.1}) : null;
228230
const rules = [xZero ? ruleX([0]) : null, yZero ? ruleY([0]) : null];
229-
mark = mark(data, markOptions);
231+
const mark = markImpl(data, transformImpl ? transformImpl(transformOptions, markOptions) : markOptions);
230232
return colorMode === "stroke" ? marks(frames, rules, mark) : marks(frames, mark, rules);
231233
}
232234

@@ -260,6 +262,7 @@ function normalizeOptions({x, y, color, size, fx, fy, mark} = {}) {
260262
if (!isOptions(size)) size = makeOptions(size);
261263
if (isOptions(fx)) ({value: fx} = makeOptions(fx));
262264
if (isOptions(fy)) ({value: fy} = makeOptions(fy));
265+
if (mark != null) mark = `${mark}`.toLowerCase();
263266
return {x, y, color, size, fx, fy, mark};
264267
}
265268

test/marks/auto-test.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ it("Plot.autoSpec makes a histogram from a quantitative dimension", () => {
66
assert.deepStrictEqual(Plot.autoSpec(data, {x: "value"}), {
77
fx: null,
88
fy: null,
9-
x: {value: "value", reduce: null},
9+
x: {value: "value", reduce: null, zero: false},
1010
y: {value: null, reduce: "count", zero: true},
1111
color: {value: null, reduce: null},
1212
size: {value: null, reduce: null},
@@ -19,7 +19,7 @@ it("Plot.autoSpec makes a bar chart from an ordinal dimension", () => {
1919
assert.deepStrictEqual(Plot.autoSpec(data, {x: "value", color: "blue"}), {
2020
fx: null,
2121
fy: null,
22-
x: {value: "value", reduce: null},
22+
x: {value: "value", reduce: null, zero: false},
2323
y: {value: null, reduce: "count", zero: true},
2424
color: {value: null, reduce: null, color: "blue"},
2525
size: {value: null, reduce: null},
@@ -36,8 +36,8 @@ it("Plot.autoSpec makes a line from a monotonic dimension", () => {
3636
assert.deepStrictEqual(Plot.autoSpec(data, {x: "date", y: "value"}), {
3737
fx: null,
3838
fy: null,
39-
x: {value: "date", reduce: null},
40-
y: {value: "value", reduce: null},
39+
x: {value: "date", reduce: null, zero: false},
40+
y: {value: "value", reduce: null, zero: false},
4141
color: {value: null, reduce: null},
4242
size: {value: null, reduce: null},
4343
mark: "line"
@@ -53,8 +53,8 @@ it("Plot.autoSpec makes a dot plot from two quantitative dimensions", () => {
5353
assert.deepStrictEqual(Plot.autoSpec(data, {x: "x", y: "y"}), {
5454
fx: null,
5555
fy: null,
56-
x: {value: "x", reduce: null},
57-
y: {value: "y", reduce: null},
56+
x: {value: "x", reduce: null, zero: false},
57+
y: {value: "y", reduce: null, zero: false},
5858
color: {value: null, reduce: null},
5959
size: {value: null, reduce: null},
6060
mark: "dot"
@@ -73,8 +73,8 @@ it("Plot.autoSpec makes a faceted heatmap", () => {
7373
assert.deepStrictEqual(Plot.autoSpec(data, {x: "x", y: "y", fy: "f", color: "count"}), {
7474
fx: null,
7575
fy: "f",
76-
x: {value: "x", reduce: null},
77-
y: {value: "y", reduce: null},
76+
x: {value: "x", reduce: null, zero: false},
77+
y: {value: "y", reduce: null, zero: false},
7878
color: {value: null, reduce: "count"},
7979
size: {value: null, reduce: null},
8080
mark: "bar"

0 commit comments

Comments
 (0)