diff --git a/src/index.js b/src/index.js
index 330fc618c2..953d7eaeef 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,7 +2,7 @@ export {plot} from "./plot.js";
export {Mark, marks} from "./mark.js";
export {Area, area, areaX, areaY} from "./marks/area.js";
export {Arrow, arrow} from "./marks/arrow.js";
-export {auto, autoSpec} from "./marks/auto.js";
+export {auto, autoSpec, autoCompile} from "./marks/auto.js";
export {axisX, axisY, axisFx, axisFy, gridX, gridY, gridFx, gridFy} from "./marks/axis.js";
export {BarX, BarY, barX, barY} from "./marks/bar.js";
export {boxX, boxY} from "./marks/box.js";
diff --git a/src/marks/auto.js b/src/marks/auto.js
index 062bd43f46..940cc560da 100644
--- a/src/marks/auto.js
+++ b/src/marks/auto.js
@@ -5,16 +5,59 @@ import {dot} from "./dot.js";
import {line, lineX, lineY} from "./line.js";
import {ruleX, ruleY} from "./rule.js";
import {barX, barY} from "./bar.js";
-import {rect, rectX, rectY} from "./rect.js";
+import {rectX, rectY} from "./rect.js";
import {cell} from "./cell.js";
import {frame} from "./frame.js";
import {bin, binX, binY} from "../transforms/bin.js";
import {group, groupX, groupY} from "../transforms/group.js";
import {marks} from "../mark.js";
+export function autoCompile(data, options) {
+ let {
+ fx,
+ fy,
+ x: {zero: xZero},
+ y: {zero: yZero},
+ markImpl,
+ markOptions,
+ transform,
+ transformOptions,
+ colorMode
+ } = autoImpl(data, options);
+
+ const markOptionsString = transform
+ ? `Plot.${transform.name}(${JSON.stringify(transformOptions)}, ${JSON.stringify(markOptions)})`
+ : JSON.stringify(markOptions);
+ const frame = fx != null || fy != null ? `Plot.frame({strokeOpacity: 0.1})` : null;
+ const rules = [xZero ? `Plot.ruleX([0])` : null, yZero ? `Plot.ruleY([0])` : null];
+ const mark = `Plot.${markImpl.name}(data, ${markOptionsString})`;
+ return `Plot.plot({
+ marks: [
+ ${(colorMode === "stroke" ? [frame, ...rules, mark] : [frame, mark, ...rules])
+ .filter((mark) => mark)
+ .join(",\n ")}
+ ]
+})`;
+}
+
export function autoSpec(data, options) {
+ const {x, y, fx, fy, color, size, mark} = autoImpl(data, options);
+ return {x, y, fx, fy, color, size, mark};
+}
+
+function autoImpl(data, options) {
options = normalizeOptions(options);
+ // Greedily materialize columns for type inference; we’ll need them anyway to
+ // plot! Note that we don’t apply any type inference to the fx and fy
+ // channels, if present; these are always ordinal (at least for now).
+ const {x, y, color, size} = options;
+ const X = materializeValue(data, x);
+ const Y = materializeValue(data, y);
+ const C = materializeValue(data, color);
+ const S = materializeValue(data, size);
+
+ // Compute the default options.
let {
fx,
fy,
@@ -25,10 +68,6 @@ export function autoSpec(data, options) {
mark
} = options;
- // Lazily materialize x and y columns for type inference, if needed.
- const {x, y} = options;
- let X, Y;
-
// Determine the default reducer, if any.
if (xReduce === undefined)
xReduce = yReduce == null && xValue == null && sizeValue == null && yValue != null ? "count" : null;
@@ -42,8 +81,8 @@ export function autoSpec(data, options) {
colorReduce == null &&
xReduce == null &&
yReduce == null &&
- (xValue == null || isOrdinal((X ??= materializeValue(data, x)))) &&
- (yValue == null || isOrdinal((Y ??= materializeValue(data, y))))
+ (xValue == null || isOrdinal(X)) &&
+ (yValue == null || isOrdinal(Y))
) {
sizeReduce = "count";
}
@@ -62,12 +101,10 @@ export function autoSpec(data, options) {
mark =
sizeValue != null || sizeReduce != null
? "dot"
- : xZero || yZero || colorReduce != null // histogram or heatmap
+ : isZeroReducer(xReduce) || isZeroReducer(yReduce) || colorReduce != null // histogram or heatmap
? "bar"
: xValue != null && yValue != null
- ? isOrdinal((X ??= materializeValue(data, x))) ||
- isOrdinal((Y ??= materializeValue(data, y))) ||
- (xReduce == null && yReduce == null && !isMonotonic(X) && !isMonotonic(Y))
+ ? isOrdinal(X) || isOrdinal(Y) || (xReduce == null && yReduce == null && !isMonotonic(X) && !isMonotonic(Y))
? "dot"
: "line"
: xValue != null || yValue != null
@@ -75,89 +112,33 @@ export function autoSpec(data, options) {
: null;
}
- return {
- fx: fx ?? null,
- fy: fy ?? null,
- x: {
- value: xValue ?? null,
- reduce: xReduce ?? null,
- ...(xZero !== undefined && {zero: xZero}), // TODO realize default
- ...xOptions
- },
- y: {
- value: yValue ?? null,
- reduce: yReduce ?? null,
- ...(yZero !== undefined && {zero: yZero}), // TODO realize default
- ...yOptions
- },
- color: {
- value: colorValue ?? null,
- reduce: colorReduce ?? null,
- ...(colorColor !== undefined && {color: colorColor})
- },
- size: {
- value: sizeValue ?? null,
- reduce: sizeReduce ?? null
- },
- mark
- };
-}
-
-export function auto(data, options) {
- options = normalizeOptions(options);
-
- // Greedily materialize columns for type inference; we’ll need them anyway to
- // plot! Note that we don’t apply any type inference to the fx and fy
- // channels, if present; these are always ordinal (at least for now).
- const {x, y, color, size} = options;
- const X = materializeValue(data, x);
- const Y = materializeValue(data, y);
- const C = materializeValue(data, color);
- const S = materializeValue(data, size);
-
- // Compute the default options via autoSpec.
- let {
- fx,
- fy,
- x: {reduce: xReduce, zero: xZero, ...xOptions},
- y: {reduce: yReduce, zero: yZero, ...yOptions},
- color: {color: colorColor, reduce: colorReduce},
- size: {reduce: sizeReduce},
- mark
- } = autoSpec(data, {
- ...options,
- x: {...x, value: X},
- y: {...y, value: Y},
- color: {...color, value: C},
- size: {...size, value: S}
- });
-
let Z; // may be set to null to disable series-by-color for line and area
let colorMode; // "fill" or "stroke"
// Determine the mark implementation.
+ let markImpl;
if (mark != null) {
switch (`${mark}`.toLowerCase()) {
case "dot":
- mark = dot;
+ markImpl = dot;
colorMode = "stroke";
break;
case "line":
- mark = X && Y ? line : X ? lineX : lineY; // 1d line by index
+ markImpl = X && Y ? line : X ? lineX : lineY; // 1d line by index
colorMode = "stroke";
if (isHighCardinality(C)) Z = null; // TODO only if z not set by user
break;
case "area":
- mark = yZero ? areaY : xZero || (Y && isMonotonic(Y)) ? areaX : areaY; // favor areaY if unsure
+ markImpl = yZero ? areaY : xZero || (Y && isMonotonic(Y)) ? areaX : areaY; // favor areaY if unsure
colorMode = "fill";
if (isHighCardinality(C)) Z = null; // TODO only if z not set by user
break;
case "rule":
- mark = X ? ruleX : ruleY;
+ markImpl = X ? ruleX : ruleY;
colorMode = "stroke";
break;
case "bar":
- mark = yZero
+ markImpl = yZero
? isOrdinalReduced(xReduce, X)
? barY
: rectY
@@ -171,7 +152,7 @@ export function auto(data, options) {
? barY
: isOrdinalReduced(yReduce, Y)
? barX
- : rect;
+ : rectY;
colorMode = "fill";
break;
default:
@@ -211,23 +192,78 @@ export function auto(data, options) {
if (transform) {
if (transform === bin || transform === binX) markOptions.x = {value: X, ...xOptions};
if (transform === bin || transform === binY) markOptions.y = {value: Y, ...yOptions};
- markOptions = transform(transformOptions, markOptions);
}
// If zero-ness is not specified, default based on whether the resolved mark
- // type will include a zero baseline. TODO Move this to autoSpec.
+ // type will include a zero baseline.
if (xZero === undefined)
- xZero = X && transform !== binX && (mark === barX || mark === areaX || mark === rectX || mark === ruleY);
+ xZero =
+ X &&
+ transform !== bin &&
+ transform !== binX &&
+ (markImpl === barX || markImpl === areaX || markImpl === rectX || markImpl === ruleY);
if (yZero === undefined)
- yZero = Y && transform !== binY && (mark === barY || mark === areaY || mark === rectY || mark === ruleX);
+ yZero =
+ Y &&
+ transform !== bin &&
+ transform !== binY &&
+ (markImpl === barY || markImpl === areaY || markImpl === rectY || markImpl === ruleX);
+
+ return {
+ fx: fx ?? null,
+ fy: fy ?? null,
+ x: {
+ value: xValue ?? null,
+ reduce: xReduce ?? null,
+ zero: xZero ?? false,
+ ...xOptions
+ },
+ y: {
+ value: yValue ?? null,
+ reduce: yReduce ?? null,
+ zero: yZero ?? false,
+ ...yOptions
+ },
+ color: {
+ value: colorValue ?? null,
+ reduce: colorReduce ?? null,
+ ...(colorColor !== undefined && {color: colorColor})
+ },
+ size: {
+ value: sizeValue ?? null,
+ reduce: sizeReduce ?? null
+ },
+ mark,
+ markImpl,
+ markOptions,
+ transform,
+ transformOptions,
+ colorMode
+ };
+}
+
+export function auto(data, options) {
+ let {
+ fx,
+ fy,
+ x: {zero: xZero},
+ y: {zero: yZero},
+ markImpl,
+ markOptions,
+ transform,
+ transformOptions,
+ colorMode
+ } = autoImpl(data, options);
+
+ if (transform) markOptions = transform(transformOptions, markOptions);
// In the case of filled marks (particularly bars and areas) the frame and
// rules should come after the mark; in the case of stroked marks
// (particularly dots and lines) they should come before the mark.
const frames = fx != null || fy != null ? frame({strokeOpacity: 0.1}) : null;
const rules = [xZero ? ruleX([0]) : null, yZero ? ruleY([0]) : null];
- mark = mark(data, markOptions);
- return colorMode === "stroke" ? marks(frames, rules, mark) : marks(frames, mark, rules);
+ markImpl = markImpl(data, markOptions);
+ return colorMode === "stroke" ? marks(frames, rules, markImpl) : marks(frames, markImpl, rules);
}
// TODO What about sorted within series?
@@ -253,6 +289,7 @@ function isMonotonic(values) {
// (but note that they can also be specified as a {transform} object such as
// Plot.identity). We don’t support reducers for the faceting, but for symmetry
// with x and y we allow facets to be specified as {value} objects.
+// TODO Normalize mark name to lowercase?
function normalizeOptions({x, y, color, size, fx, fy, mark} = {}) {
if (!isOptions(x)) x = makeOptions(x);
if (!isOptions(y)) y = makeOptions(y);
diff --git a/test/marks/auto-test.js b/test/marks/auto-test.js
index 88ccb00036..c9a0a4b187 100644
--- a/test/marks/auto-test.js
+++ b/test/marks/auto-test.js
@@ -1,12 +1,25 @@
import * as Plot from "@observablehq/plot";
import assert from "assert";
+it("Plot.autoCompile compiles to explicit marks", () => {
+ const data = [{value: 1}, {value: 1}, {value: 38}];
+ assert.strictEqual(
+ Plot.autoCompile(data, {x: "value"}),
+ `Plot.plot({
+ marks: [
+ Plot.rectY(data, Plot.binX({"y":"count"}, {"x":{"value":[1,1,38]}})),
+ Plot.ruleY([0])
+ ]
+})`
+ );
+});
+
it("Plot.autoSpec makes a histogram from a quantitative dimension", () => {
const data = [{value: 1}, {value: 1}, {value: 38}];
assert.deepStrictEqual(Plot.autoSpec(data, {x: "value"}), {
fx: null,
fy: null,
- x: {value: "value", reduce: null},
+ x: {value: "value", reduce: null, zero: false},
y: {value: null, reduce: "count", zero: true},
color: {value: null, reduce: null},
size: {value: null, reduce: null},
@@ -19,7 +32,7 @@ it("Plot.autoSpec makes a bar chart from an ordinal dimension", () => {
assert.deepStrictEqual(Plot.autoSpec(data, {x: "value", color: "blue"}), {
fx: null,
fy: null,
- x: {value: "value", reduce: null},
+ x: {value: "value", reduce: null, zero: false},
y: {value: null, reduce: "count", zero: true},
color: {value: null, reduce: null, color: "blue"},
size: {value: null, reduce: null},
@@ -36,8 +49,8 @@ it("Plot.autoSpec makes a line from a monotonic dimension", () => {
assert.deepStrictEqual(Plot.autoSpec(data, {x: "date", y: "value"}), {
fx: null,
fy: null,
- x: {value: "date", reduce: null},
- y: {value: "value", reduce: null},
+ x: {value: "date", reduce: null, zero: false},
+ y: {value: "value", reduce: null, zero: false},
color: {value: null, reduce: null},
size: {value: null, reduce: null},
mark: "line"
@@ -53,8 +66,8 @@ it("Plot.autoSpec makes a dot plot from two quantitative dimensions", () => {
assert.deepStrictEqual(Plot.autoSpec(data, {x: "x", y: "y"}), {
fx: null,
fy: null,
- x: {value: "x", reduce: null},
- y: {value: "y", reduce: null},
+ x: {value: "x", reduce: null, zero: false},
+ y: {value: "y", reduce: null, zero: false},
color: {value: null, reduce: null},
size: {value: null, reduce: null},
mark: "dot"
@@ -73,8 +86,8 @@ it("Plot.autoSpec makes a faceted heatmap", () => {
assert.deepStrictEqual(Plot.autoSpec(data, {x: "x", y: "y", fy: "f", color: "count"}), {
fx: null,
fy: "f",
- x: {value: "x", reduce: null},
- y: {value: "y", reduce: null},
+ x: {value: "x", reduce: null, zero: false},
+ y: {value: "y", reduce: null, zero: false},
color: {value: null, reduce: "count"},
size: {value: null, reduce: null},
mark: "bar"
diff --git a/test/output/autoBarNonZeroReducer.svg b/test/output/autoBarNonZeroReducer.svg
new file mode 100644
index 0000000000..c8915a1b2c
--- /dev/null
+++ b/test/output/autoBarNonZeroReducer.svg
@@ -0,0 +1,124 @@
+
\ No newline at end of file
diff --git a/test/output/autoBarZero.svg b/test/output/autoBarZero.svg
deleted file mode 100644
index 5b63abcdf7..0000000000
--- a/test/output/autoBarZero.svg
+++ /dev/null
@@ -1,127 +0,0 @@
-
\ No newline at end of file
diff --git a/test/output/autoDotZero.svg b/test/output/autoDotZero.svg
new file mode 100644
index 0000000000..a1837e91d1
--- /dev/null
+++ b/test/output/autoDotZero.svg
@@ -0,0 +1,127 @@
+
\ No newline at end of file
diff --git a/test/output/autoBarMeanZero.svg b/test/output/autoLineMeanZero.svg
similarity index 55%
rename from test/output/autoBarMeanZero.svg
rename to test/output/autoLineMeanZero.svg
index 5e4b404754..09fed388cc 100644
--- a/test/output/autoBarMeanZero.svg
+++ b/test/output/autoLineMeanZero.svg
@@ -47,38 +47,19 @@
↑ temp_max
-
-
-
-
-
+
+
+
- 2012
- 2013
- 2014
- 2015
- 2016
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ 2013
+ 2014
+ 2015
+
+
+
\ No newline at end of file
diff --git a/test/output/autoLineZero.svg b/test/output/autoLineZero.svg
new file mode 100644
index 0000000000..911d370189
--- /dev/null
+++ b/test/output/autoLineZero.svg
@@ -0,0 +1,94 @@
+
\ No newline at end of file
diff --git a/test/plots/autoplot.ts b/test/plots/autoplot.ts
index 85d8195ae6..a5a63c169c 100644
--- a/test/plots/autoplot.ts
+++ b/test/plots/autoplot.ts
@@ -1,6 +1,18 @@
import * as Plot from "@observablehq/plot";
import * as d3 from "d3";
+// Tanner's bug https://github.com/observablehq/plot/issues/1365
+export async function autoLineZero() {
+ const industries = await d3.csv("data/bls-industry-unemployment.csv", d3.autoType);
+ return Plot.auto(industries, {x: "date", y: {value: "unemployed", zero: true}, color: "industry"}).plot();
+}
+
+// Jeff's bug https://github.com/observablehq/plot/issues/1340
+export async function autoBarNonZeroReducer() {
+ const athletes = await d3.csv("data/athletes.csv", d3.autoType);
+ return Plot.auto(athletes, {x: "date_of_birth", y: {value: "height", reduce: "mean"}, mark: "bar"}).plot();
+}
+
export async function autoHistogram() {
const athletes = await d3.csv("data/athletes.csv", d3.autoType);
return Plot.auto(athletes, {x: "weight"}).plot();
@@ -81,14 +93,14 @@ export async function autoDotOrdCont() {
return Plot.auto(penguins, {x: "culmen_length_mm", y: "species"}).plot();
}
-export async function autoBar() {
+export async function autoDotZero() {
const alphabet = await d3.csv("data/alphabet.csv", d3.autoType);
- return Plot.auto(alphabet, {x: "frequency", y: "letter", mark: "bar"}).plot();
+ return Plot.auto(alphabet, {x: {value: "frequency", zero: true}, y: "letter"}).plot();
}
-export async function autoBarZero() {
+export async function autoBar() {
const alphabet = await d3.csv("data/alphabet.csv", d3.autoType);
- return Plot.auto(alphabet, {x: {value: "frequency", zero: true}, y: "letter"}).plot();
+ return Plot.auto(alphabet, {x: "frequency", y: "letter", mark: "bar"}).plot();
}
export async function autoConnectedScatterplot() {
@@ -197,11 +209,6 @@ export async function autoBarStackColorConstant() {
return Plot.auto(athletes, {x: "height", color: "gold"}).plot();
}
-export async function autoBarMeanZero() {
- const weather = await d3.csv("data/seattle-weather.csv", d3.autoType);
- return Plot.auto(weather, {x: "date", y: {value: "temp_max", reduce: "mean", zero: true}}).plot();
-}
-
export async function autoBarMode() {
const penguins = await d3.csv("data/penguins.csv", d3.autoType);
return Plot.auto(penguins, {x: "island", y: {value: "species", reduce: "mode"}, mark: "bar"}).plot();
@@ -212,6 +219,11 @@ export async function autoLineMean() {
return Plot.auto(weather, {x: "date", y: {value: "temp_max", reduce: "mean"}}).plot();
}
+export async function autoLineMeanZero() {
+ const weather = await d3.csv("data/seattle-weather.csv", d3.autoType);
+ return Plot.auto(weather, {x: "date", y: {value: "temp_max", reduce: "mean", zero: true}}).plot();
+}
+
export async function autoLineMeanColor() {
const athletes = await d3.csv("data/athletes.csv", d3.autoType);
return Plot.auto(athletes, {x: "date_of_birth", y: {value: "height", reduce: "mean"}, color: "sex"}).plot();