Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
197 changes: 117 additions & 80 deletions src/marks/auto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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";
}
Expand All @@ -62,102 +101,44 @@ 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
? "rule"
: 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
Expand All @@ -171,7 +152,7 @@ export function auto(data, options) {
? barY
: isOrdinalReduced(yReduce, Y)
? barX
: rect;
: rectY;
colorMode = "fill";
break;
default:
Expand Down Expand Up @@ -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?
Expand All @@ -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);
Expand Down
29 changes: 21 additions & 8 deletions test/marks/auto-test.js
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -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},
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand Down
Loading