From aa0ba22dba2fddb552e97fc815e34b02fca8c75c Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 6 Jun 2025 00:19:06 +0200 Subject: [PATCH 1/3] fix: areaY dense interval without explicit y option (#2328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed areaY to work with dense intervals (interval + reduce) without requiring an explicit y option, matching lineY behavior. 🤖 Generated with [Claude Code](https://claude.ai/code) Total cost: $5.51 Total duration (API): 23m 31.3s Total duration (wall): 14h 30m 4.3s Total code changes: 208 lines added, 53 lines removed Token usage by model: claude-3-5-haiku: 204.7k input, 4.8k output, 0 cache read, 0 cache write claude-sonnet: 18.9k input, 28.4k output, 8.3m cache read, 629.5k cache write --- src/marks/area.js | 10 +++-- test/output/denseIntervalAreaY.svg | 64 +++++++++++++++++++++++++++ test/output/denseIntervalLineY.svg | 70 ++++++++++++++++++++++++++++++ test/plots/dense-interval.ts | 16 +++++++ test/plots/index.ts | 1 + 5 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 test/output/denseIntervalAreaY.svg create mode 100644 test/output/denseIntervalLineY.svg create mode 100644 test/plots/dense-interval.ts diff --git a/src/marks/area.js b/src/marks/area.js index 628088445a..8299cf0469 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -2,7 +2,7 @@ import {area as shapeArea} from "d3"; import {create} from "../context.js"; import {maybeCurve} from "../curve.js"; import {Mark} from "../mark.js"; -import {first, indexOf, maybeZ, second} from "../options.js"; +import {first, identity, indexOf, maybeZ, second} from "../options.js"; import { applyDirectStyles, applyIndirectStyles, @@ -77,11 +77,15 @@ export function area(data, options) { } export function areaX(data, options) { - const {y = indexOf, ...rest} = maybeDenseIntervalY(options); + const {y = indexOf, ...rest} = maybeDenseIntervalY( + options?.interval != null ? {...options, x: options.x ?? identity} : options + ); return new Area(data, maybeStackX(maybeIdentityX({...rest, y1: y, y2: undefined}, y === indexOf ? "x2" : "x"))); } export function areaY(data, options) { - const {x = indexOf, ...rest} = maybeDenseIntervalX(options); + const {x = indexOf, ...rest} = maybeDenseIntervalX( + options?.interval != null ? {...options, y: options.y ?? identity} : options + ); return new Area(data, maybeStackY(maybeIdentityY({...rest, x1: x, x2: undefined}, x === indexOf ? "y2" : "y"))); } diff --git a/test/output/denseIntervalAreaY.svg b/test/output/denseIntervalAreaY.svg new file mode 100644 index 0000000000..dc3d752c73 --- /dev/null +++ b/test/output/denseIntervalAreaY.svg @@ -0,0 +1,64 @@ + + + + + 0 + 2 + 4 + 6 + 8 + 10 + 12 + 14 + 16 + 18 + 20 + 22 + + + ↑ Frequency + + + + 2014 + 2015 + 2016 + 2017 + 2018 + + + + + \ No newline at end of file diff --git a/test/output/denseIntervalLineY.svg b/test/output/denseIntervalLineY.svg new file mode 100644 index 0000000000..6a09fb2bc8 --- /dev/null +++ b/test/output/denseIntervalLineY.svg @@ -0,0 +1,70 @@ + + + + + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + 22 + 23 + + + ↑ Frequency + + + + 2014 + 2015 + 2016 + 2017 + 2018 + + + + + \ No newline at end of file diff --git a/test/plots/dense-interval.ts b/test/plots/dense-interval.ts new file mode 100644 index 0000000000..f68bd468bd --- /dev/null +++ b/test/plots/dense-interval.ts @@ -0,0 +1,16 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export async function denseIntervalAreaY() { + const aapl = await d3.csv("data/aapl.csv", d3.autoType); + return Plot.plot({ + marks: [Plot.areaY(aapl, {x: "Date", reduce: "count", interval: "month"})] + }); +} + +export async function denseIntervalLineY() { + const aapl = await d3.csv("data/aapl.csv", d3.autoType); + return Plot.plot({ + marks: [Plot.lineY(aapl, {x: "Date", reduce: "count", interval: "month"})] + }); +} diff --git a/test/plots/index.ts b/test/plots/index.ts index f54398dca7..996f68f03f 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -48,6 +48,7 @@ export * from "./boxplot.js"; export * from "./caltrain-direction.js"; export * from "./caltrain.js"; export * from "./cars-dodge.js"; +export * from "./dense-interval.js"; export * from "./cars-hexbin.js"; export * from "./cars-jitter.js"; export * from "./cars-mpg.js"; From 936bdd8f157f8b641d77310dfe6683d3e028c636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 6 Jun 2025 14:57:32 +0200 Subject: [PATCH 2/3] a human fix: when y is undefined, default to y: reduce --- src/marks/area.js | 10 +++------- src/transforms/bin.js | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/marks/area.js b/src/marks/area.js index 8299cf0469..628088445a 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -2,7 +2,7 @@ import {area as shapeArea} from "d3"; import {create} from "../context.js"; import {maybeCurve} from "../curve.js"; import {Mark} from "../mark.js"; -import {first, identity, indexOf, maybeZ, second} from "../options.js"; +import {first, indexOf, maybeZ, second} from "../options.js"; import { applyDirectStyles, applyIndirectStyles, @@ -77,15 +77,11 @@ export function area(data, options) { } export function areaX(data, options) { - const {y = indexOf, ...rest} = maybeDenseIntervalY( - options?.interval != null ? {...options, x: options.x ?? identity} : options - ); + const {y = indexOf, ...rest} = maybeDenseIntervalY(options); return new Area(data, maybeStackX(maybeIdentityX({...rest, y1: y, y2: undefined}, y === indexOf ? "x2" : "x"))); } export function areaY(data, options) { - const {x = indexOf, ...rest} = maybeDenseIntervalX( - options?.interval != null ? {...options, y: options.y ?? identity} : options - ); + const {x = indexOf, ...rest} = maybeDenseIntervalX(options); return new Area(data, maybeStackY(maybeIdentityY({...rest, x1: x, x2: undefined}, x === indexOf ? "y2" : "y"))); } diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 02321a3345..5962d84fe7 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -71,7 +71,7 @@ function maybeDenseInterval(bin, k, options = {}) { if (options?.interval == null) return options; const {reduce = reduceFirst} = options; const outputs = {filter: null}; - if (options[k] != null) outputs[k] = reduce; + if (options[k] !== null) outputs[k] = reduce; if (options[`${k}1`] != null) outputs[`${k}1`] = reduce; if (options[`${k}2`] != null) outputs[`${k}2`] = reduce; return bin(outputs, options); From c0108339eb0c679f4b8a188da48e727fb9a71329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 6 Jun 2025 15:15:07 +0200 Subject: [PATCH 3/3] add vertical tests --- test/output/denseIntervalAreaX.svg | 64 ++++++++++++++++++++++++++++++ test/output/denseIntervalLineX.svg | 64 ++++++++++++++++++++++++++++++ test/plots/dense-interval.ts | 24 +++++++++++ 3 files changed, 152 insertions(+) create mode 100644 test/output/denseIntervalAreaX.svg create mode 100644 test/output/denseIntervalLineX.svg diff --git a/test/output/denseIntervalAreaX.svg b/test/output/denseIntervalAreaX.svg new file mode 100644 index 0000000000..7dc2c7ccb1 --- /dev/null +++ b/test/output/denseIntervalAreaX.svg @@ -0,0 +1,64 @@ + + + + + −3.5 + −3.0 + −2.5 + −2.0 + −1.5 + −1.0 + −0.5 + 0.0 + 0.5 + 1.0 + 1.5 + 2.0 + 2.5 + 3.0 + + + + 0 + 100 + 200 + + + Frequency → + + + + + \ No newline at end of file diff --git a/test/output/denseIntervalLineX.svg b/test/output/denseIntervalLineX.svg new file mode 100644 index 0000000000..9238917254 --- /dev/null +++ b/test/output/denseIntervalLineX.svg @@ -0,0 +1,64 @@ + + + + + −3.5 + −3.0 + −2.5 + −2.0 + −1.5 + −1.0 + −0.5 + 0.0 + 0.5 + 1.0 + 1.5 + 2.0 + 2.5 + 3.0 + + + + 0 + 100 + 200 + + + Frequency → + + + + + \ No newline at end of file diff --git a/test/plots/dense-interval.ts b/test/plots/dense-interval.ts index f68bd468bd..196376f9a9 100644 --- a/test/plots/dense-interval.ts +++ b/test/plots/dense-interval.ts @@ -14,3 +14,27 @@ export async function denseIntervalLineY() { marks: [Plot.lineY(aapl, {x: "Date", reduce: "count", interval: "month"})] }); } + +export async function denseIntervalAreaX() { + return Plot.areaX( + {length: 1000}, + { + y: d3.randomNormal.source(d3.randomLcg(42))(), + reduce: "count", + interval: 0.5, + curve: "basis" + } + ).plot({width: 200}); +} + +export async function denseIntervalLineX() { + return Plot.lineX( + {length: 1000}, + { + y: d3.randomNormal.source(d3.randomLcg(42))(), + reduce: "count", + interval: 0.5, + curve: "basis" + } + ).plot({width: 200}); +}