diff --git a/package.json b/package.json
index f176ce9a96..a344b65f1f 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-terser": "^0.4.0",
"@types/d3": "^7.4.0",
+ "@types/mocha": "^10.0.1",
"@types/node": "^20.5.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
@@ -77,8 +78,13 @@
},
"c8": {
"all": true,
- "include": ["src/**/*.js"],
- "reporter": ["text", "lcov"]
+ "include": [
+ "src/**/*.js"
+ ],
+ "reporter": [
+ "text",
+ "lcov"
+ ]
},
"dependencies": {
"d3": "^7.8.0",
diff --git a/test/mark-test.ts b/test/mark-test.ts
new file mode 100644
index 0000000000..8624a8b1d5
--- /dev/null
+++ b/test/mark-test.ts
@@ -0,0 +1,17 @@
+import * as Plot from "@observablehq/plot";
+import assert from "assert";
+import it from "./jsdom.js";
+
+it("mark(data, {sort}) needs y1 and y2 when sorting by height", () => {
+ assert.throws(() => Plot.dot([], {sort: {x: "height"}}).plot(), /^Error: missing channel: y1$/);
+ assert.throws(() => Plot.dot([], {channels: {y1: "1"}, sort: {x: "height"}}).plot(), /^Error: missing channel: y2$/);
+});
+
+it("mark(data, {sort}) needs x1 and x2 when sorting by width", () => {
+ assert.throws(() => Plot.dot([], {sort: {y: "width"}}).plot(), /^Error: missing channel: x1$/);
+ assert.throws(() => Plot.dot([], {channels: {x1: "1"}, sort: {y: "width"}}).plot(), /^Error: missing channel: x2$/);
+});
+
+it("mark(data, {sort}) rejects an invalid order", () => {
+ assert.throws(() => Plot.dotY([0, 1], {sort: {y: {value: "x", order: "neo" as any}}}).plot(), /^Error: invalid order: neo$/); // prettier-ignore
+});
diff --git a/test/marks/line-test.js b/test/marks/line-test.js
index d60c58fc20..a66fdec79e 100644
--- a/test/marks/line-test.js
+++ b/test/marks/line-test.js
@@ -1,5 +1,6 @@
import * as Plot from "@observablehq/plot";
import {curveStep} from "d3";
+import {curveAuto} from "../../src/curve.js";
import assert from "assert";
it("line() has the expected defaults", () => {
@@ -114,3 +115,12 @@ it("line(data, {curve}) specifies a named curve or function", () => {
assert.strictEqual(Plot.line(undefined, {curve: "step"}).curve, curveStep);
assert.strictEqual(Plot.line(undefined, {curve: curveStep}).curve, curveStep);
});
+
+it("line(data, {curve}) rejects an invalid curve", () => {
+ assert.throws(() => Plot.lineY([], {y: 1, curve: "neo"}), /^Error: unknown curve: neo$/);
+ assert.throws(() => Plot.lineY([], {y: 1, curve: 42}), /^Error: unknown curve: 42$/);
+});
+
+it("line(data, {curve}) accepts the explicit auto curve", () => {
+ assert.strictEqual(Plot.lineY([], {y: 1, curve: "auto"}).curve, curveAuto);
+});
diff --git a/test/output/curves.svg b/test/output/curves.svg
new file mode 100644
index 0000000000..30f5f54719
--- /dev/null
+++ b/test/output/curves.svg
@@ -0,0 +1,83 @@
+
\ No newline at end of file
diff --git a/test/plot-test.ts b/test/plot-test.ts
new file mode 100644
index 0000000000..baedd583ae
--- /dev/null
+++ b/test/plot-test.ts
@@ -0,0 +1,8 @@
+import * as Plot from "@observablehq/plot";
+import assert from "assert";
+import it from "./jsdom.js";
+
+it("plot({aspectRatio}) rejects unsupported scale types", () => {
+ assert.throws(() => Plot.dot([]).plot({aspectRatio: true, x: {type: "symlog"}}), /^Error: unsupported x scale for aspectRatio: symlog$/); // prettier-ignore
+ assert.throws(() => Plot.dot([]).plot({aspectRatio: true, y: {type: "symlog"}}), /^Error: unsupported y scale for aspectRatio: symlog$/); // prettier-ignore
+});
diff --git a/test/plots/curves.ts b/test/plots/curves.ts
new file mode 100644
index 0000000000..eb7381c1d3
--- /dev/null
+++ b/test/plots/curves.ts
@@ -0,0 +1,26 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+export async function curves() {
+ const random = d3.randomLcg(42);
+ const values = d3.ticks(0, 1, 11).map((t) => {
+ const r = 1 + 2 * random();
+ return [r * Math.cos(t * 2 * Math.PI), r * Math.sin(t * 2 * Math.PI)];
+ });
+ return Plot.plot({
+ width: 500,
+ axis: null,
+ aspectRatio: true,
+ inset: 10,
+ marks: [
+ d3
+ .ticks(0, 1, 4)
+ .map((tension) => [
+ Plot.line(values, {curve: "bundle", tension, stroke: "red", mixBlendMode: "multiply"}),
+ Plot.line(values, {curve: "cardinal-closed", tension, stroke: "green", mixBlendMode: "multiply"}),
+ Plot.line(values, {curve: "catmull-rom-closed", tension, stroke: "blue", mixBlendMode: "multiply"})
+ ]),
+ Plot.dot(values, {stroke: "white", fill: "black"})
+ ]
+ });
+}
diff --git a/test/plots/index.ts b/test/plots/index.ts
index 82b3c2e88a..f54ad0948a 100644
--- a/test/plots/index.ts
+++ b/test/plots/index.ts
@@ -58,6 +58,7 @@ export * from "./crimean-war-line.js";
export * from "./crimean-war-overlapped.js";
export * from "./crimean-war-stacked.js";
export * from "./crosshair.js";
+export * from "./curves.js";
export * from "./d3-survey-2015-comfort.js";
export * from "./d3-survey-2015-why.js";
export * from "./darker-dodge.js";
diff --git a/test/plots/us-congress-age-color-explicit.ts b/test/plots/us-congress-age-color-explicit.ts
index a359e90825..4eff8cd376 100644
--- a/test/plots/us-congress-age-color-explicit.ts
+++ b/test/plots/us-congress-age-color-explicit.ts
@@ -5,15 +5,8 @@ export async function usCongressAgeColorExplicit() {
const data = await d3.csv("data/us-congress-members.csv", d3.autoType);
return Plot.plot({
height: 300,
- x: {
- nice: true,
- label: "Age",
- labelAnchor: "right"
- },
- y: {
- grid: true,
- label: "Frequency"
- },
+ x: {nice: true, label: "Age"},
+ y: {grid: true, label: "Frequency"},
marks: [
Plot.dot(
data,
diff --git a/test/plots/us-congress-age-gender.ts b/test/plots/us-congress-age-gender.ts
index cc8f6331dd..c3f1962a39 100644
--- a/test/plots/us-congress-age-gender.ts
+++ b/test/plots/us-congress-age-gender.ts
@@ -5,11 +5,7 @@ export async function usCongressAgeGender() {
const data = await d3.csv("data/us-congress-members.csv", d3.autoType);
return Plot.plot({
height: 300,
- x: {
- nice: true,
- label: "Age",
- labelAnchor: "right"
- },
+ x: {nice: true, label: "Age"},
y: {
grid: true,
label: "← Women · Men →",
diff --git a/test/plots/us-congress-age-symbol-explicit.ts b/test/plots/us-congress-age-symbol-explicit.ts
index 9d9b9d6fe0..9802ebe826 100644
--- a/test/plots/us-congress-age-symbol-explicit.ts
+++ b/test/plots/us-congress-age-symbol-explicit.ts
@@ -5,15 +5,8 @@ export async function usCongressAgeSymbolExplicit() {
const data = await d3.csv("data/us-congress-members.csv", d3.autoType);
return Plot.plot({
height: 300,
- x: {
- nice: true,
- label: "Age",
- labelAnchor: "right"
- },
- y: {
- grid: true,
- label: "Frequency"
- },
+ x: {nice: true, label: "Age"},
+ y: {grid: true, label: "Frequency"},
marks: [
Plot.dot(
data,
diff --git a/test/plots/us-congress-age.ts b/test/plots/us-congress-age.ts
index a6e45f695e..8697c79f89 100644
--- a/test/plots/us-congress-age.ts
+++ b/test/plots/us-congress-age.ts
@@ -5,15 +5,8 @@ export async function usCongressAge() {
const data = await d3.csv("data/us-congress-members.csv", d3.autoType);
return Plot.plot({
height: 300,
- x: {
- nice: true,
- label: "Age",
- labelAnchor: "right"
- },
- y: {
- grid: true,
- label: "Frequency"
- },
+ x: {nice: true, label: "Age"},
+ y: {grid: true, label: "Frequency"},
marks: [
Plot.dot(data, Plot.stackY2({x: (d) => 2021 - d.birth, fill: "currentColor", title: "full_name"})),
Plot.ruleY([0])
diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js
index 5135cec632..faa4ab6d1b 100644
--- a/test/scales/scales-test.js
+++ b/test/scales/scales-test.js
@@ -2192,6 +2192,54 @@ it("plot(…).scale(name) returns a deduplicated ordinal/temporal domain", () =>
});
});
+it("mark(data, {channels}) respects a scale set to undefined", () => {
+ assert.strictEqual(
+ Plot.dot({length: 1}, {channels: {fill: {value: ["red"]}}}).initialize().channels.fill.scale,
+ undefined
+ );
+ assert.strictEqual(
+ Plot.dot({length: 1}, {channels: {fill: {value: ["foo"]}}}).initialize().channels.fill.scale,
+ undefined
+ );
+});
+
+it("mark(data, {channels}) respects a scale set to auto", () => {
+ assert.strictEqual(
+ Plot.dot({length: 1}, {channels: {fill: {value: ["red"], scale: "auto"}}}).initialize().channels.fill.scale,
+ null
+ );
+ assert.strictEqual(
+ Plot.dot({length: 1}, {channels: {fill: {value: ["foo"], scale: "auto"}}}).initialize().channels.fill.scale,
+ "color"
+ );
+});
+
+it("mark(data, {channels}) respects a scale set to true or false", () => {
+ assert.strictEqual(
+ Plot.dot({length: 1}, {channels: {fill: {value: ["red"], scale: true}}}).initialize().channels.fill.scale,
+ "color"
+ );
+ assert.strictEqual(
+ Plot.dot({length: 1}, {channels: {fill: {value: ["red"], scale: false}}}).initialize().channels.fill.scale,
+ null
+ );
+ assert.strictEqual(
+ Plot.dot({length: 1}, {channels: {fill: {value: ["foo"], scale: true}}}).initialize().channels.fill.scale,
+ "color"
+ );
+ assert.strictEqual(
+ Plot.dot({length: 1}, {channels: {fill: {value: ["foo"], scale: false}}}).initialize().channels.fill.scale,
+ null
+ );
+});
+
+it("mark(data, {channels}) rejects unknown scales", () => {
+ assert.throws(
+ () => Plot.dot([], {channels: {fill: {value: (d) => d, scale: "neo"}}}).initialize().channels.fill.scale,
+ /^Error: unknown scale: neo$/
+ );
+});
+
// Given a plot specification (or, as shorthand, an array of marks or a single
// mark), asserts that the given named scales, when materialized from the first
// plot and used to produce a second plot, produce the same output and the same
diff --git a/test/transforms/stack-test.js b/test/transforms/stack-test.ts
similarity index 56%
rename from test/transforms/stack-test.js
rename to test/transforms/stack-test.ts
index c64f57face..df23b4f6da 100644
--- a/test/transforms/stack-test.js
+++ b/test/transforms/stack-test.ts
@@ -1,15 +1,19 @@
import * as Plot from "@observablehq/plot";
import assert from "assert";
-it("Plot.stack returns the expected values", () => {
+it("stackY(options) returns the expected values", () => {
const y = [1, 2, -2, -1];
const {
channels: {
y1: {value: Y1},
y2: {value: Y2}
}
- } = Plot.barY(y, Plot.stackY({y})).initialize();
+ } = (Plot.barY(y, Plot.stackY({y})) as any).initialize();
assert.deepStrictEqual(y, [1, 2, -2, -1]);
assert.deepStrictEqual(Y1, Float64Array.of(0, 1, 0, -2));
assert.deepStrictEqual(Y2, Float64Array.of(1, 3, -2, -3));
});
+
+it("stackY({order}) rejects an invalid order", () => {
+ assert.throws(() => Plot.barY([], {y: 1, order: 42 as any}), /^Error: invalid order: 42$/);
+});
diff --git a/yarn.lock b/yarn.lock
index c45db2c2b9..62e89e1b36 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -815,6 +815,11 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
+"@types/mocha@^10.0.1":
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.1.tgz#2f4f65bb08bc368ac39c96da7b2f09140b26851b"
+ integrity sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==
+
"@types/node@^20.5.0":
version "20.5.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.0.tgz#7fc8636d5f1aaa3b21e6245e97d56b7f56702313"