Skip to content

Commit cc1824b

Browse files
tophtuckermbostock
andauthored
Auto mark (#1238)
* Auto mark, working, with one test * more tests * pReTtIeR * even pRetTIer * Nullish checks on sizeValue and sizeReduce Co-authored-by: Mike Bostock <[email protected]> * Facet frame style matches default grid style; more concise transform options * isHighCardinality helper * More permissive shorthand * move isHighCardinality; adopt identity * rewrap a few comments * constant-color disambiguation * remove completed TODO * auto zero; auto z-order for decos * sort exports * Fixing marks import; regenerating tests * Default size reduce * Rm todo * Remove shorthand for now :( * rm unused import * error on null x & y, too * fix facet axis labels * require opposite channel when reducing --------- Co-authored-by: Mike Bostock <[email protected]>
1 parent b97ba31 commit cc1824b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+34842
-0
lines changed

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export {plot} from "./plot.js";
22
export {Mark, marks} from "./mark.js";
33
export {Area, area, areaX, areaY} from "./marks/area.js";
44
export {Arrow, arrow} from "./marks/arrow.js";
5+
export {auto} from "./marks/auto.js";
56
export {axisX, axisY, axisFx, axisFy, gridX, gridY, gridFx, gridFy} from "./marks/axis.js";
67
export {BarX, BarY, barX, barY} from "./marks/bar.js";
78
export {boxX, boxY} from "./marks/box.js";

src/marks/auto.js

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import {InternSet} from "d3";
2+
import {isOrdinal, labelof, valueof, isOptions, isColor} from "../options.js";
3+
import {area, 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";
7+
import {barX, barY} from "./bar.js";
8+
import {rect, rectX, rectY} from "./rect.js";
9+
import {cell} from "./cell.js";
10+
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";
14+
import {ascending} from "d3";
15+
16+
export function auto(data, {x, y, color, size, fx, fy, mark} = {}) {
17+
// Allow x and y and other dimensions to be specified as shorthand field names
18+
// (but note that they can also be specified as a {transform} object such as
19+
// Plot.identity).
20+
if (!isOptions(x)) x = makeOptions(x);
21+
if (!isOptions(y)) y = makeOptions(y);
22+
if (!isOptions(color)) color = isColor(color) ? {color} : makeOptions(color);
23+
if (!isOptions(size)) size = makeOptions(size);
24+
25+
// We don’t apply any type inference to the fx and fy channels, if present, so
26+
// these are simply passed-through to the underlying mark’s options.
27+
if (isOptions(fx)) ({value: fx} = makeOptions(fx));
28+
if (isOptions(fy)) ({value: fy} = makeOptions(fy));
29+
30+
const {value: xValue} = x;
31+
const {value: yValue} = y;
32+
const {value: sizeValue} = size;
33+
const {value: colorValue, color: colorColor} = color;
34+
35+
// Determine the default reducer, if any.
36+
let {reduce: xReduce} = x;
37+
let {reduce: yReduce} = y;
38+
let {reduce: sizeReduce} = size;
39+
let {reduce: colorReduce} = color;
40+
if (xReduce === undefined)
41+
xReduce = yReduce == null && xValue == null && sizeValue == null && yValue != null ? "count" : null;
42+
if (yReduce === undefined)
43+
yReduce = xReduce == null && yValue == null && sizeValue == null && xValue != null ? "count" : null;
44+
45+
let {zero: xZero} = x;
46+
let {zero: yZero} = y;
47+
48+
// TODO The line mark will need z?
49+
// TODO Limit and sort for bar charts (e.g. alphabet)?
50+
// TODO Look at Plot warnings and see how many we can prevent
51+
// TODO Default to something other than turbo for continuous? Like:
52+
// scheme: (colorValue && isContinuous(color)) || colorReduce ? "ylgnbu" : undefined
53+
54+
// To apply heuristics based on the data types (values), realize the columns.
55+
// We could maybe look at the data.schema here, but Plot’s behavior depends on
56+
// the actual values anyway, so this probably is what we want. By
57+
// materializing the columns here, we also ensure that they aren’t re-computed
58+
// later in Plot.plot.
59+
x = valueof(data, xValue);
60+
y = valueof(data, yValue);
61+
color = valueof(data, colorValue);
62+
size = valueof(data, sizeValue);
63+
64+
// TODO Shorthand: array of primitives should result in a histogram
65+
if (!x && !y) throw new Error("must specify x or y");
66+
if (xReduce != null && !y) throw new Error("reducing x requires y");
67+
if (yReduce != null && !x) throw new Error("reducing y requires x");
68+
69+
let z, zReduce;
70+
71+
// Propagate the x and y labels (field names), if any. This is necessary for
72+
// any column we materialize (and hence, we don’t need to do this for fx and
73+
// fy, since those columns are not needed for type inference and hence are not
74+
// greedily materialized).
75+
if (x) x.label = labelof(xValue);
76+
if (y) y.label = labelof(yValue);
77+
if (color) color.label = labelof(colorValue);
78+
if (size) size.label = labelof(sizeValue);
79+
80+
// Determine the default size reducer, if any.
81+
if (
82+
sizeReduce === undefined &&
83+
sizeValue == null &&
84+
colorReduce == null &&
85+
xReduce == null &&
86+
yReduce == null &&
87+
(!x || isOrdinal(x)) &&
88+
(!y || isOrdinal(y))
89+
) {
90+
sizeReduce = "count";
91+
}
92+
93+
// Determine the default mark type.
94+
if (mark === undefined) {
95+
mark =
96+
sizeValue != null || sizeReduce != null
97+
? "dot"
98+
: xReduce != null || yReduce != null || colorReduce != null
99+
? "bar"
100+
: x && y
101+
? isContinuous(x) && isContinuous(y) && (isMonotonic(x) || isMonotonic(y))
102+
? "line"
103+
: (isContinuous(x) && xZero) || (isContinuous(y) && yZero)
104+
? "bar"
105+
: "dot"
106+
: x
107+
? "rule"
108+
: y
109+
? "rule"
110+
: null;
111+
}
112+
113+
let colorMode; // "fill" or "stroke"
114+
115+
// Determine the mark implementation.
116+
if (mark != null) {
117+
switch (`${mark}`.toLowerCase()) {
118+
case "dot":
119+
mark = dot;
120+
colorMode = "stroke";
121+
break;
122+
case "line":
123+
mark = x && y ? line : x ? lineX : lineY; // 1d line by index
124+
colorMode = "stroke";
125+
if (isHighCardinality(color)) z = null; // TODO only if z not set by user
126+
break;
127+
case "area":
128+
mark =
129+
x && y
130+
? isContinuous(x) && isMonotonic(x)
131+
? areaY
132+
: isContinuous(y) && isMonotonic(y)
133+
? areaX
134+
: area // TODO error? how does it work with ordinal?
135+
: x
136+
? areaX
137+
: areaY; // 1d area by index
138+
colorMode = "fill";
139+
if (isHighCardinality(color)) z = null; // TODO only if z not set by user
140+
break;
141+
case "rule":
142+
mark = x ? ruleX : ruleY;
143+
colorMode = "stroke";
144+
break;
145+
case "bar":
146+
mark =
147+
yReduce != null
148+
? isOrdinal(x)
149+
? barY
150+
: rectY
151+
: xReduce != null
152+
? isOrdinal(y)
153+
? barX
154+
: rectX
155+
: colorReduce != null
156+
? x && y && isOrdinal(x) && isOrdinal(y)
157+
? cell
158+
: x && isOrdinal(x)
159+
? barY
160+
: y && isOrdinal(y)
161+
? barX
162+
: rect
163+
: x && y && isOrdinal(x) && isOrdinal(y)
164+
? cell
165+
: y && isOrdinal(y)
166+
? barX
167+
: barY;
168+
colorMode = "fill";
169+
break;
170+
default:
171+
throw new Error(`invalid mark: ${mark}`);
172+
}
173+
}
174+
175+
// Determine the mark options.
176+
let options = {x, y, [colorMode]: color ?? colorColor, z, r: size, fx, fy};
177+
let transform;
178+
let transformOptions = {[colorMode]: colorReduce, z: zReduce, r: sizeReduce};
179+
if (xReduce != null && yReduce != null) {
180+
throw new Error(`cannot reduce both x and y`); // for now at least
181+
} else if (yReduce != null) {
182+
transformOptions.y = yReduce;
183+
transform = isOrdinal(x) ? groupX : binX;
184+
} else if (xReduce != null) {
185+
transformOptions.x = xReduce;
186+
transform = isOrdinal(y) ? groupY : binY;
187+
} else if (colorReduce != null || sizeReduce != null) {
188+
if (x && y) {
189+
transform = isOrdinal(x) && isOrdinal(y) ? group : isOrdinal(x) ? binY : isOrdinal(y) ? binX : bin;
190+
} else if (x) {
191+
transform = isOrdinal(x) ? groupX : binX;
192+
} else if (y) {
193+
transform = isOrdinal(y) ? groupY : binY;
194+
}
195+
}
196+
if (transform) options = transform(transformOptions, options);
197+
198+
// If zero-ness is not specified, default based on whether the resolved mark
199+
// type will include a zero baseline.
200+
if (xZero === undefined) xZero = transform !== binX && (mark === barX || mark === areaX || mark === rectX);
201+
if (yZero === undefined) yZero = transform !== binY && (mark === barY || mark === areaY || mark === rectY);
202+
203+
// In the case of filled marks (particularly bars and areas) the frame and
204+
// rules should come after the mark; in the case of stroked marks
205+
// (particularly dots and lines) they should come before the mark.
206+
const frames = fx != null || fy != null ? frame({strokeOpacity: 0.1}) : null;
207+
const rules = [xZero ? ruleX([0]) : null, yZero ? ruleY([0]) : null];
208+
mark = mark(data, options);
209+
return colorMode === "stroke" ? marks(frames, rules, mark) : marks(frames, mark, rules);
210+
}
211+
212+
function isContinuous(values) {
213+
return !isOrdinal(values);
214+
}
215+
216+
// TODO What about sorted within series?
217+
function isMonotonic(values) {
218+
let previous;
219+
let previousOrder;
220+
for (const value of values) {
221+
if (value == null) continue;
222+
if (previous === undefined) {
223+
previous = value;
224+
continue;
225+
}
226+
const order = Math.sign(ascending(previous, value));
227+
if (!order) continue; // skip zero, NaN
228+
if (previousOrder !== undefined && order !== previousOrder) return false;
229+
previous = value;
230+
previousOrder = order;
231+
}
232+
return true;
233+
}
234+
235+
function makeOptions(value) {
236+
return isReducer(value) ? {reduce: value} : {value};
237+
}
238+
239+
// https://github.com/observablehq/plot/blob/818562649280e155136f730fc496e0b3d15ae464/src/transforms/group.js#L236
240+
function isReducer(reduce) {
241+
if (typeof reduce?.reduce === "function") return true;
242+
if (/^p\d{2}$/i.test(reduce)) return true;
243+
switch (`${reduce}`.toLowerCase()) {
244+
case "first":
245+
case "last":
246+
case "count":
247+
case "distinct":
248+
case "sum":
249+
case "proportion":
250+
case "proportion-facet": // TODO remove me?
251+
case "deviation":
252+
case "min":
253+
case "min-index": // TODO remove me?
254+
case "max":
255+
case "max-index": // TODO remove me?
256+
case "mean":
257+
case "median":
258+
case "variance":
259+
case "mode":
260+
// These are technically reducers, but I think we’d want to treat them as fields?
261+
// case "x":
262+
// case "x1":
263+
// case "x2":
264+
// case "y":
265+
// case "y1":
266+
// case "y2":
267+
return true;
268+
}
269+
return false;
270+
}
271+
272+
function isHighCardinality(value) {
273+
return value ? new InternSet(value).size > value.length >> 1 : false;
274+
}

test/output/autoArea.svg

Lines changed: 63 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)