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"