From 4568a906e4a2bc544b00c504a41ce68279a30cd8 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 5 Apr 2026 13:22:32 -0400 Subject: [PATCH 1/8] arealine --- src/index.d.ts | 1 + src/index.js | 1 + src/marks/area.js | 4 +- src/marks/areaLine.d.ts | 40 ++++++++++++++++++++ src/marks/areaLine.js | 80 +++++++++++++++++++++++++++++++++++++++ test/output/aaplClose.svg | 10 ++--- test/plots/aapl-close.ts | 3 +- 7 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 src/marks/areaLine.d.ts create mode 100644 src/marks/areaLine.js diff --git a/src/index.d.ts b/src/index.d.ts index a83f0f3715..016d89d678 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -10,6 +10,7 @@ export * from "./legends.js"; export * from "./mark.js"; export * from "./marker.js"; export * from "./marks/area.js"; +export * from "./marks/areaLine.js"; export * from "./marks/arrow.js"; export * from "./marks/auto.js"; export * from "./marks/axis.js"; diff --git a/src/index.js b/src/index.js index a95fdbc035..4853d4ddc1 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ Mark.prototype.plot = function ({marks = [], ...options} = {}) { export {plot} from "./plot.js"; export {Mark, marks} from "./mark.js"; export {Area, area, areaX, areaY} from "./marks/area.js"; +export {AreaLine, areaLineX, areaLineY} from "./marks/areaLine.js"; export {Arrow, arrow} from "./marks/arrow.js"; export {auto, autoSpec} from "./marks/auto.js"; export {axisX, axisY, axisFx, axisFy, gridX, gridY, gridFx, gridFy} from "./marks/axis.js"; diff --git a/src/marks/area.js b/src/marks/area.js index f820302a27..d2e836389c 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -8,7 +8,7 @@ import {groupIndex} from "../style.js"; import {maybeDenseIntervalX, maybeDenseIntervalY} from "../transforms/bin.js"; import {maybeStackX, maybeStackY} from "../transforms/stack.js"; -const defaults = { +const areaDefaults = { ariaLabel: "area", strokeWidth: 1, strokeLinecap: "round", @@ -17,7 +17,7 @@ const defaults = { }; export class Area extends Mark { - constructor(data, options = {}) { + constructor(data, options = {}, defaults = areaDefaults) { const {x1, y1, x2, y2, z, curve, tension} = options; super( data, diff --git a/src/marks/areaLine.d.ts b/src/marks/areaLine.d.ts new file mode 100644 index 0000000000..3dcd0e6a1f --- /dev/null +++ b/src/marks/areaLine.d.ts @@ -0,0 +1,40 @@ +import type {Data} from "../mark.js"; +import {MarkerOptions} from "../marker.js"; +import {Area, AreaXOptions, AreaYOptions} from "./area.js"; + +/** Options for the areaLineX mark. */ +export interface AreaLineXOptions extends AreaXOptions, MarkerOptions {} + +/** Options for the areaLineY mark. */ +export interface AreaLineYOptions extends AreaYOptions, MarkerOptions {} + +/** + * Returns a new vertically-oriented arealine mark for the given *data* and + * *options*, where the baseline and topline share **y** values, as in a + * time-series area chart where time goes up↑. For example, to plot Apple’s + * daily stock price: + * + * ```js + * Plot.areaLineX(aapl, {y: "Date", x: "Close"}) + * ``` + * + * See Plot.areaX for more details. + */ +export function areaLineX(data?: Data, options?: AreaLineXOptions): AreaLine; + +/** + * Returns a new horizontally-oriented arealine mark for the given *data* and + * *options*, where the baseline and topline share **x** values, as in a + * time-series area chart where time goes right→. For example, to plot Apple’s + * daily stock price: + * + * ```js + * Plot.areaLineY(aapl, {x: "Date", y: "Close"}) + * ``` + * + * See Plot.areaLineY for more details. + */ +export function areaLineY(data?: Data, options?: AreaLineYOptions): AreaLine; + +/** The arealine mark. */ +export class AreaLine extends Area {} diff --git a/src/marks/areaLine.js b/src/marks/areaLine.js new file mode 100644 index 0000000000..406e55a24f --- /dev/null +++ b/src/marks/areaLine.js @@ -0,0 +1,80 @@ +import {area as shapeArea, line as shapeLine} from "d3"; +import {Area} from "./area.js"; +import {create} from "../context.js"; +import {applyGroupedMarkers, markers} from "../marker.js"; +import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js"; +import {groupIndex} from "../style.js"; +import {maybeDenseIntervalX, maybeDenseIntervalY} from "../transforms/bin.js"; +import {maybeStackX, maybeStackY} from "../transforms/stack.js"; + +const defaults = { + ariaLabel: "area-line", + fillOpacity: 0.1, + stroke: "currentColor", + strokeWidth: 1.5, + strokeLinecap: "round", + strokeLinejoin: "round", + strokeMiterlimit: 1 +}; + +export class AreaLine extends Area { + constructor(data, options = {}) { + super(data, options, defaults); + markers(this, options); + } + render(index, scales, channels, dimensions, context) { + const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels; + return create("svg:g", context) + .call(applyIndirectStyles, this, dimensions, context) + .call(applyTransform, this, scales, 0, 0) + .call((g) => + g + .selectAll() + .data(groupIndex(index, [X1, Y1, X2, Y2], this, channels)) + .enter() + .append("g") + .call(applyDirectStyles, this) + .call(applyGroupedChannelStyles, this, channels) + .call((e) => + e + .append("path") + .attr("stroke", "none") + .attr( + "d", + shapeArea() + .curve(this.curve) + .defined((i) => i >= 0) + .x0((i) => X1[i]) + .y0((i) => Y1[i]) + .x1((i) => X2[i]) + .y1((i) => Y2[i]) + ) + ) + .call((e) => + e + .append("path") + .call(applyGroupedMarkers, this, channels, context) + .attr("fill", "none") + .attr( + "d", + shapeLine() + .curve(this.curve) + .defined((i) => i >= 0) + .x((i) => X2[i]) + .y((i) => Y2[i]) + ) + ) + ) + .node(); + } +} + +export function areaLineX(data, options) { + const {x, y, fill, z = x === fill ? null : undefined, ...rest} = maybeDenseIntervalY(options); + return new AreaLine(data, maybeStackX({...rest, x, y1: y, y2: undefined, z, fill})); +} + +export function areaLineY(data, options) { + const {x, y, fill, z = y === fill ? null : undefined, ...rest} = maybeDenseIntervalX(options); + return new AreaLine(data, maybeStackY({...rest, x1: x, x2: undefined, y, z, fill})); +} diff --git a/test/output/aaplClose.svg b/test/output/aaplClose.svg index 5fb51a1e86..66b35a35d5 100644 --- a/test/output/aaplClose.svg +++ b/test/output/aaplClose.svg @@ -66,11 +66,11 @@ 2017 2018 - - - - - + + + + + diff --git a/test/plots/aapl-close.ts b/test/plots/aapl-close.ts index a764857dac..1803713850 100644 --- a/test/plots/aapl-close.ts +++ b/test/plots/aapl-close.ts @@ -7,8 +7,7 @@ test(async function aaplClose() { return Plot.plot({ y: {grid: true}, marks: [ - Plot.areaY(aapl, {x: "Date", y: "Close", fillOpacity: 0.1}), - Plot.lineY(aapl, {x: "Date", y: "Close"}), + Plot.areaLineY(aapl, {x: "Date", y: "Close"}), Plot.ruleY([0]) ] }); From a6deb1cc1c1367746110cf077e4c2bb3813e12ff Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 5 Apr 2026 13:41:44 -0400 Subject: [PATCH 2/8] color option shorthand --- src/mark.d.ts | 5 +++++ src/marks/areaLine.d.ts | 6 +++--- src/marks/areaLine.js | 8 ++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/mark.d.ts b/src/mark.d.ts index 43e61da15f..9fc092f45e 100644 --- a/src/mark.d.ts +++ b/src/mark.d.ts @@ -473,6 +473,11 @@ export interface MarkOptions { channels?: Record; } +export interface ColorOptions { + /** Shorthand for setting both the fill and the stroke. */ + color?: ChannelValueSpec; +} + /** The abstract base class for Mark implementations. */ export class Mark { /** diff --git a/src/marks/areaLine.d.ts b/src/marks/areaLine.d.ts index 3dcd0e6a1f..97fd9e8579 100644 --- a/src/marks/areaLine.d.ts +++ b/src/marks/areaLine.d.ts @@ -1,12 +1,12 @@ -import type {Data} from "../mark.js"; +import type {ColorOptions, Data} from "../mark.js"; import {MarkerOptions} from "../marker.js"; import {Area, AreaXOptions, AreaYOptions} from "./area.js"; /** Options for the areaLineX mark. */ -export interface AreaLineXOptions extends AreaXOptions, MarkerOptions {} +export interface AreaLineXOptions extends AreaXOptions, ColorOptions, MarkerOptions {} /** Options for the areaLineY mark. */ -export interface AreaLineYOptions extends AreaYOptions, MarkerOptions {} +export interface AreaLineYOptions extends AreaYOptions, ColorOptions, MarkerOptions {} /** * Returns a new vertically-oriented arealine mark for the given *data* and diff --git a/src/marks/areaLine.js b/src/marks/areaLine.js index 406e55a24f..7d64d66f21 100644 --- a/src/marks/areaLine.js +++ b/src/marks/areaLine.js @@ -70,11 +70,11 @@ export class AreaLine extends Area { } export function areaLineX(data, options) { - const {x, y, fill, z = x === fill ? null : undefined, ...rest} = maybeDenseIntervalY(options); - return new AreaLine(data, maybeStackX({...rest, x, y1: y, y2: undefined, z, fill})); + const {x, y, color, stroke = color, fill = color, z = x === fill || x === stroke ? null : undefined, ...rest} = maybeDenseIntervalY(options); // prettier-ignore + return new AreaLine(data, maybeStackX({...rest, x, y1: y, y2: undefined, z, fill, stroke})); } export function areaLineY(data, options) { - const {x, y, fill, z = y === fill ? null : undefined, ...rest} = maybeDenseIntervalX(options); - return new AreaLine(data, maybeStackY({...rest, x1: x, x2: undefined, y, z, fill})); + const {x, y, color, stroke = color, fill = color, z = y === fill || y === stroke ? null : undefined, ...rest} = maybeDenseIntervalX(options); // prettier-ignore + return new AreaLine(data, maybeStackY({...rest, x1: x, x2: undefined, y, z, fill, stroke})); } From 3829e997ce6c045c6bd5df63b84b9bea93d6ce8a Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 5 Apr 2026 15:57:12 -0400 Subject: [PATCH 3/8] fix line offset --- src/marks/areaLine.js | 3 ++- test/output/aaplClose.svg | 2 +- test/plots/aapl-close.ts | 5 +---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/marks/areaLine.js b/src/marks/areaLine.js index 7d64d66f21..e1c9514bbe 100644 --- a/src/marks/areaLine.js +++ b/src/marks/areaLine.js @@ -3,7 +3,7 @@ import {Area} from "./area.js"; import {create} from "../context.js"; import {applyGroupedMarkers, markers} from "../marker.js"; import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js"; -import {groupIndex} from "../style.js"; +import {groupIndex, offset} from "../style.js"; import {maybeDenseIntervalX, maybeDenseIntervalY} from "../transforms/bin.js"; import {maybeStackX, maybeStackY} from "../transforms/stack.js"; @@ -55,6 +55,7 @@ export class AreaLine extends Area { .append("path") .call(applyGroupedMarkers, this, channels, context) .attr("fill", "none") + .attr("transform", offset ? `translate(${offset},${offset})` : null) .attr( "d", shapeLine() diff --git a/test/output/aaplClose.svg b/test/output/aaplClose.svg index 66b35a35d5..11e8483aaa 100644 --- a/test/output/aaplClose.svg +++ b/test/output/aaplClose.svg @@ -69,7 +69,7 @@ - + diff --git a/test/plots/aapl-close.ts b/test/plots/aapl-close.ts index 1803713850..0676004373 100644 --- a/test/plots/aapl-close.ts +++ b/test/plots/aapl-close.ts @@ -6,10 +6,7 @@ test(async function aaplClose() { const aapl = await d3.csv("data/aapl.csv", d3.autoType); return Plot.plot({ y: {grid: true}, - marks: [ - Plot.areaLineY(aapl, {x: "Date", y: "Close"}), - Plot.ruleY([0]) - ] + marks: [Plot.areaLineY(aapl, {x: "Date", y: "Close"}), Plot.ruleY([0])] }); }); From cd44c86676b2e41d3e9392557125726dc163888e Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 5 Apr 2026 19:51:48 -0400 Subject: [PATCH 4/8] document area line mark --- docs/data/api.data.ts | 2 ++ docs/marks/area.md | 30 +++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/docs/data/api.data.ts b/docs/data/api.data.ts index cbcd4b2139..cc50949b51 100644 --- a/docs/data/api.data.ts +++ b/docs/data/api.data.ts @@ -50,6 +50,8 @@ function getHref(name: string, path: string): string { return `${path}s`; case "features/options": return "features/transforms"; + case "marks/areaLine": + return "marks/area"; case "marks/axis": { switch (name) { case "gridX": diff --git a/docs/marks/area.md b/docs/marks/area.md index e967127b5d..a2ec997b77 100644 --- a/docs/marks/area.md +++ b/docs/marks/area.md @@ -20,7 +20,7 @@ Plot.areaY(aapl, {x: "Date", y: "Close"}).plot() The area mark has three constructors: [areaY](#areaY) for when the baseline and topline share *x* values, as in a time-series area chart where time goes right→ (or ←left); [areaX](#areaX) for when the baseline and topline share *y* values, as in a time-series area chart where time goes up↑ (or down↓); and lastly the rarely-used [area](#area) where the baseline and topline share neither *x* nor *y* values. -The area mark is often paired with a [line](./line.md) and [rule](./rule.md) mark to accentuate the topline and baseline. +The **area line mark** is a variant of the area mark that accentuates the topline. It is often paired with a [rule](./rule.md) mark for the baseline. :::plot https://observablehq.com/@observablehq/plot-area-and-line ```js @@ -29,8 +29,7 @@ Plot.plot({ grid: true }, marks: [ - Plot.areaY(aapl, {x: "Date", y: "Close", fillOpacity: 0.3}), - Plot.lineY(aapl, {x: "Date", y: "Close"}), + Plot.areaLineY(aapl, {x: "Date", y: "Close"}), Plot.ruleY([0]) ] }) @@ -94,8 +93,7 @@ Plot.plot({ reverse: true }, marks: [ - Plot.areaY(aapl, {x: "Date", y: "Close", fillOpacity: 0.3}), - Plot.lineY(aapl, {x: "Date", y: "Close"}), + Plot.areaLineY(aapl, {x: "Date", y: "Close"}), Plot.ruleY([0]) ] }) @@ -111,8 +109,7 @@ Plot.plot({ grid: true }, marks: [ - Plot.areaX(aapl, {y: "Date", x: "Close", fillOpacity: 0.3}), - Plot.lineX(aapl, {y: "Date", x: "Close"}), + Plot.areaLineX(aapl, {y: "Date", x: "Close"}), Plot.ruleX([0]) ] }) @@ -128,8 +125,7 @@ Plot.plot({ grid: true }, marks: [ - Plot.areaY(aapl, {x: "Date", y: (d) => d.Date.getUTCMonth() < 3 ? NaN : d.Close, fillOpacity: 0.3}), - Plot.lineY(aapl, {x: "Date", y: (d) => d.Date.getUTCMonth() < 3 ? NaN : d.Close}), + Plot.areaLineY(aapl, {x: "Date", y: (d) => d.Date.getUTCMonth() < 3 ? NaN : d.Close}), Plot.ruleY([0]) ] }) @@ -326,6 +322,22 @@ Plot.areaX(observations, {y: "date", x: "temperature", interval: "day"}) The **interval** option is recommended to “regularize” sampled data; for example, if your data represents timestamped temperature measurements and you expect one sample per day, use "day" as the interval. +## areaLineY(*data*, *options*) {#areaLineY} + +```js +Plot.areaLineY(aapl, {x: "Date", y: "Close"}) +``` + +A variant of [areaY](#areaY) that accentuates the topline. + +## areaLineX(*data*, *options*) {#areaLineX} + +```js +Plot.areaLineX(aapl, {y: "Date", x: "Close"}) +``` + +A variant of [areaX](#areaX) that accentuates the topline. + ## area(*data*, *options*) {#area} ```js From ac22d706268950ecf490b72b4fedbac7e0753044 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 5 Apr 2026 20:10:34 -0400 Subject: [PATCH 5/8] fix docs:build? --- docs/data/api.data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/api.data.ts b/docs/data/api.data.ts index cc50949b51..8e4b2bf3e4 100644 --- a/docs/data/api.data.ts +++ b/docs/data/api.data.ts @@ -50,7 +50,7 @@ function getHref(name: string, path: string): string { return `${path}s`; case "features/options": return "features/transforms"; - case "marks/areaLine": + case "marks/area-line": return "marks/area"; case "marks/axis": { switch (name) { From 9d024cc37b476ba631c7fda38653603ecdfa2d04 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 6 Apr 2026 09:04:54 -0400 Subject: [PATCH 6/8] restore line option; fillOpacity 0.3 --- docs/data/api.data.ts | 2 - docs/marks/area.md | 28 ++++---------- src/index.d.ts | 1 - src/index.js | 1 - src/marks/area.d.ts | 12 ++++-- src/marks/area.js | 76 +++++++++++++++++++++++++++++++++--- src/marks/areaLine.d.ts | 40 ------------------- src/marks/areaLine.js | 81 --------------------------------------- test/output/aaplClose.svg | 2 +- test/plots/aapl-close.ts | 2 +- 10 files changed, 87 insertions(+), 158 deletions(-) delete mode 100644 src/marks/areaLine.d.ts delete mode 100644 src/marks/areaLine.js diff --git a/docs/data/api.data.ts b/docs/data/api.data.ts index 8e4b2bf3e4..cbcd4b2139 100644 --- a/docs/data/api.data.ts +++ b/docs/data/api.data.ts @@ -50,8 +50,6 @@ function getHref(name: string, path: string): string { return `${path}s`; case "features/options": return "features/transforms"; - case "marks/area-line": - return "marks/area"; case "marks/axis": { switch (name) { case "gridX": diff --git a/docs/marks/area.md b/docs/marks/area.md index a2ec997b77..767d6f9bd6 100644 --- a/docs/marks/area.md +++ b/docs/marks/area.md @@ -20,7 +20,7 @@ Plot.areaY(aapl, {x: "Date", y: "Close"}).plot() The area mark has three constructors: [areaY](#areaY) for when the baseline and topline share *x* values, as in a time-series area chart where time goes right→ (or ←left); [areaX](#areaX) for when the baseline and topline share *y* values, as in a time-series area chart where time goes up↑ (or down↓); and lastly the rarely-used [area](#area) where the baseline and topline share neither *x* nor *y* values. -The **area line mark** is a variant of the area mark that accentuates the topline. It is often paired with a [rule](./rule.md) mark for the baseline. +The **line** option strokes the topline. It is often paired with a [rule](./rule.md) mark to denote the baseline. :::plot https://observablehq.com/@observablehq/plot-area-and-line ```js @@ -29,7 +29,7 @@ Plot.plot({ grid: true }, marks: [ - Plot.areaLineY(aapl, {x: "Date", y: "Close"}), + Plot.areaY(aapl, {x: "Date", y: "Close", line: true}), Plot.ruleY([0]) ] }) @@ -93,7 +93,7 @@ Plot.plot({ reverse: true }, marks: [ - Plot.areaLineY(aapl, {x: "Date", y: "Close"}), + Plot.areaY(aapl, {x: "Date", y: "Close", line: true}), Plot.ruleY([0]) ] }) @@ -109,7 +109,7 @@ Plot.plot({ grid: true }, marks: [ - Plot.areaLineX(aapl, {y: "Date", x: "Close"}), + Plot.areaX(aapl, {y: "Date", x: "Close", line: true}), Plot.ruleX([0]) ] }) @@ -125,7 +125,7 @@ Plot.plot({ grid: true }, marks: [ - Plot.areaLineY(aapl, {x: "Date", y: (d) => d.Date.getUTCMonth() < 3 ? NaN : d.Close}), + Plot.areaY(aapl, {x: "Date", y: (d) => d.Date.getUTCMonth() < 3 ? NaN : d.Close, line: true}), Plot.ruleY([0]) ] }) @@ -304,7 +304,7 @@ Plot.areaY(observations, {x: "date", y: "temperature", interval: "day"}) The **interval** option is recommended to “regularize” sampled data; for example, if your data represents timestamped temperature measurements and you expect one sample per day, use "day" as the interval. -The **areaY** mark draws the region between a baseline (*y1*) and a topline (*y2*) as in an area chart. When the baseline is *y* = 0, the *y* channel can be specified instead of *y1* and *y2*. +The **areaY** mark draws the region between a vertically-separated baseline (*y1*) and topline (*y2*) as in an area chart. When the baseline is *y* = 0, the *y* channel can be specified instead of *y1* and *y2*. If the **line** option is true, the **stroke** applies exclusively to the topline, and the **fillOpacity** defaults to 0.3. ## areaX(*data*, *options*) {#areaX} @@ -322,21 +322,7 @@ Plot.areaX(observations, {y: "date", x: "temperature", interval: "day"}) The **interval** option is recommended to “regularize” sampled data; for example, if your data represents timestamped temperature measurements and you expect one sample per day, use "day" as the interval. -## areaLineY(*data*, *options*) {#areaLineY} - -```js -Plot.areaLineY(aapl, {x: "Date", y: "Close"}) -``` - -A variant of [areaY](#areaY) that accentuates the topline. - -## areaLineX(*data*, *options*) {#areaLineX} - -```js -Plot.areaLineX(aapl, {y: "Date", x: "Close"}) -``` - -A variant of [areaX](#areaX) that accentuates the topline. +The **areaX** mark draws the region between a horizontally-separated baseline (*x1*) and topline (*x2*) as in a vertical area chart. When the baseline is *x* = 0, the *x* channel can be specified instead of *x1* and *x2*. If the **line** option is true, the **stroke** applies exclusively to the topline, and the **fillOpacity** defaults to 0.3. ## area(*data*, *options*) {#area} diff --git a/src/index.d.ts b/src/index.d.ts index 016d89d678..a83f0f3715 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -10,7 +10,6 @@ export * from "./legends.js"; export * from "./mark.js"; export * from "./marker.js"; export * from "./marks/area.js"; -export * from "./marks/areaLine.js"; export * from "./marks/arrow.js"; export * from "./marks/auto.js"; export * from "./marks/axis.js"; diff --git a/src/index.js b/src/index.js index 4853d4ddc1..a95fdbc035 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,6 @@ Mark.prototype.plot = function ({marks = [], ...options} = {}) { export {plot} from "./plot.js"; export {Mark, marks} from "./mark.js"; export {Area, area, areaX, areaY} from "./marks/area.js"; -export {AreaLine, areaLineX, areaLineY} from "./marks/areaLine.js"; export {Arrow, arrow} from "./marks/arrow.js"; export {auto, autoSpec} from "./marks/auto.js"; export {axisX, axisY, axisFx, axisFy, gridX, gridY, gridFx, gridFy} from "./marks/axis.js"; diff --git a/src/marks/area.d.ts b/src/marks/area.d.ts index 49aa3011be..9c1dffb748 100644 --- a/src/marks/area.d.ts +++ b/src/marks/area.d.ts @@ -1,11 +1,12 @@ import type {ChannelValue, ChannelValueDenseBinSpec, ChannelValueSpec} from "../channel.js"; import type {CurveOptions} from "../curve.js"; -import type {Data, MarkOptions, RenderableMark} from "../mark.js"; +import type {ColorOptions, Data, MarkOptions, RenderableMark} from "../mark.js"; +import {MarkerOptions} from "../marker.js"; import type {BinOptions, BinReducer} from "../transforms/bin.js"; import type {StackOptions} from "../transforms/stack.js"; /** Options for the area, areaX, and areaY marks. */ -export interface AreaOptions extends MarkOptions, StackOptions, CurveOptions { +export interface AreaOptions extends MarkOptions, StackOptions, ColorOptions, CurveOptions { /** * The required primary (starting, often left) horizontal position channel, * representing the area’s baseline, typically bound to the *x* scale. For @@ -124,6 +125,9 @@ export interface AreaYOptions extends Omit, BinOptions reduce?: BinReducer; } +/** The area mark’s line option. */ +export type AreaLineOptions = {line?: false} | ({line: true} & MarkerOptions); + /** * Returns a new area mark with the given *data* and *options*. The area mark is * rarely used directly; it is only needed when the baseline and topline have @@ -163,7 +167,7 @@ export function area(data?: Data, options?: AreaOptions): Area; * channels. When any of these channels are used, setting an explicit **z** * channel (possibly to null) is strongly recommended. */ -export function areaX(data?: Data, options?: AreaXOptions): Area; +export function areaX(data?: Data, options?: AreaXOptions & AreaLineOptions): Area; /** * Returns a new horizontally-oriented area mark for the given *data* and @@ -195,7 +199,7 @@ export function areaX(data?: Data, options?: AreaXOptions): Area; * channels. When any of these channels are used, setting an explicit **z** * channel (possibly to null) is strongly recommended. */ -export function areaY(data?: Data, options?: AreaYOptions): Area; +export function areaY(data?: Data, options?: AreaYOptions & AreaLineOptions): Area; /** The area mark. */ export class Area extends RenderableMark {} diff --git a/src/marks/area.js b/src/marks/area.js index d2e836389c..420e8c89b6 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -1,10 +1,11 @@ -import {area as shapeArea} from "d3"; +import {area as shapeArea, line as shapeLine} from "d3"; import {create} from "../context.js"; import {maybeCurve} from "../curve.js"; import {Mark} from "../mark.js"; +import {applyGroupedMarkers, markers} from "../marker.js"; import {first, maybeZ, second} from "../options.js"; import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js"; -import {groupIndex} from "../style.js"; +import {groupIndex, offset} from "../style.js"; import {maybeDenseIntervalX, maybeDenseIntervalY} from "../transforms/bin.js"; import {maybeStackX, maybeStackY} from "../transforms/stack.js"; @@ -16,6 +17,16 @@ const areaDefaults = { strokeMiterlimit: 1 }; +const areaLineDefaults = { + ariaLabel: "area-line", + fillOpacity: 0.3, + stroke: "currentColor", + strokeWidth: 1.5, + strokeLinecap: "round", + strokeLinejoin: "round", + strokeMiterlimit: 1 +}; + export class Area extends Mark { constructor(data, options = {}, defaults = areaDefaults) { const {x1, y1, x2, y2, z, curve, tension} = options; @@ -65,17 +76,70 @@ export class Area extends Mark { } } +class AreaLine extends Area { + constructor(data, options = {}) { + super(data, options, areaLineDefaults); + markers(this, options); + } + render(index, scales, channels, dimensions, context) { + const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels; + return create("svg:g", context) + .call(applyIndirectStyles, this, dimensions, context) + .call(applyTransform, this, scales, 0, 0) + .call((g) => + g + .selectAll() + .data(groupIndex(index, [X1, Y1, X2, Y2], this, channels)) + .enter() + .append("g") + .call(applyDirectStyles, this) + .call(applyGroupedChannelStyles, this, channels) + .call((e) => + e + .append("path") + .attr("stroke", "none") + .attr( + "d", + shapeArea() + .curve(this.curve) + .defined((i) => i >= 0) + .x0((i) => X1[i]) + .y0((i) => Y1[i]) + .x1((i) => X2[i]) + .y1((i) => Y2[i]) + ) + ) + .call((e) => + e + .append("path") + .call(applyGroupedMarkers, this, channels, context) + .attr("fill", "none") + .attr("transform", offset ? `translate(${offset},${offset})` : null) + .attr( + "d", + shapeLine() + .curve(this.curve) + .defined((i) => i >= 0) + .x((i) => X2[i]) + .y((i) => Y2[i]) + ) + ) + ) + .node(); + } +} + export function area(data, options) { if (options === undefined) return areaY(data, {x: first, y: second}); return new Area(data, options); } export function areaX(data, options) { - const {x, y, fill, z = x === fill ? null : undefined, ...rest} = maybeDenseIntervalY(options); - return new Area(data, maybeStackX({...rest, x, y1: y, y2: undefined, z, fill})); + const {x, y, line, color, stroke = color, fill = color, z = x === fill || (line && x === stroke) ? null : undefined, ...rest} = maybeDenseIntervalY(options); // prettier-ignore + return new (line ? AreaLine : Area)(data, maybeStackX({...rest, x, y1: y, y2: undefined, z, stroke, fill})); } export function areaY(data, options) { - const {x, y, fill, z = y === fill ? null : undefined, ...rest} = maybeDenseIntervalX(options); - return new Area(data, maybeStackY({...rest, x1: x, x2: undefined, y, z, fill})); + const {x, y, line, color, stroke = color, fill = color, z = y === fill || (line && y === stroke) ? null : undefined, ...rest} = maybeDenseIntervalX(options); // prettier-ignore + return new (line ? AreaLine : Area)(data, maybeStackY({...rest, x1: x, x2: undefined, y, z, stroke, fill})); } diff --git a/src/marks/areaLine.d.ts b/src/marks/areaLine.d.ts deleted file mode 100644 index 97fd9e8579..0000000000 --- a/src/marks/areaLine.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type {ColorOptions, Data} from "../mark.js"; -import {MarkerOptions} from "../marker.js"; -import {Area, AreaXOptions, AreaYOptions} from "./area.js"; - -/** Options for the areaLineX mark. */ -export interface AreaLineXOptions extends AreaXOptions, ColorOptions, MarkerOptions {} - -/** Options for the areaLineY mark. */ -export interface AreaLineYOptions extends AreaYOptions, ColorOptions, MarkerOptions {} - -/** - * Returns a new vertically-oriented arealine mark for the given *data* and - * *options*, where the baseline and topline share **y** values, as in a - * time-series area chart where time goes up↑. For example, to plot Apple’s - * daily stock price: - * - * ```js - * Plot.areaLineX(aapl, {y: "Date", x: "Close"}) - * ``` - * - * See Plot.areaX for more details. - */ -export function areaLineX(data?: Data, options?: AreaLineXOptions): AreaLine; - -/** - * Returns a new horizontally-oriented arealine mark for the given *data* and - * *options*, where the baseline and topline share **x** values, as in a - * time-series area chart where time goes right→. For example, to plot Apple’s - * daily stock price: - * - * ```js - * Plot.areaLineY(aapl, {x: "Date", y: "Close"}) - * ``` - * - * See Plot.areaLineY for more details. - */ -export function areaLineY(data?: Data, options?: AreaLineYOptions): AreaLine; - -/** The arealine mark. */ -export class AreaLine extends Area {} diff --git a/src/marks/areaLine.js b/src/marks/areaLine.js deleted file mode 100644 index e1c9514bbe..0000000000 --- a/src/marks/areaLine.js +++ /dev/null @@ -1,81 +0,0 @@ -import {area as shapeArea, line as shapeLine} from "d3"; -import {Area} from "./area.js"; -import {create} from "../context.js"; -import {applyGroupedMarkers, markers} from "../marker.js"; -import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js"; -import {groupIndex, offset} from "../style.js"; -import {maybeDenseIntervalX, maybeDenseIntervalY} from "../transforms/bin.js"; -import {maybeStackX, maybeStackY} from "../transforms/stack.js"; - -const defaults = { - ariaLabel: "area-line", - fillOpacity: 0.1, - stroke: "currentColor", - strokeWidth: 1.5, - strokeLinecap: "round", - strokeLinejoin: "round", - strokeMiterlimit: 1 -}; - -export class AreaLine extends Area { - constructor(data, options = {}) { - super(data, options, defaults); - markers(this, options); - } - render(index, scales, channels, dimensions, context) { - const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels; - return create("svg:g", context) - .call(applyIndirectStyles, this, dimensions, context) - .call(applyTransform, this, scales, 0, 0) - .call((g) => - g - .selectAll() - .data(groupIndex(index, [X1, Y1, X2, Y2], this, channels)) - .enter() - .append("g") - .call(applyDirectStyles, this) - .call(applyGroupedChannelStyles, this, channels) - .call((e) => - e - .append("path") - .attr("stroke", "none") - .attr( - "d", - shapeArea() - .curve(this.curve) - .defined((i) => i >= 0) - .x0((i) => X1[i]) - .y0((i) => Y1[i]) - .x1((i) => X2[i]) - .y1((i) => Y2[i]) - ) - ) - .call((e) => - e - .append("path") - .call(applyGroupedMarkers, this, channels, context) - .attr("fill", "none") - .attr("transform", offset ? `translate(${offset},${offset})` : null) - .attr( - "d", - shapeLine() - .curve(this.curve) - .defined((i) => i >= 0) - .x((i) => X2[i]) - .y((i) => Y2[i]) - ) - ) - ) - .node(); - } -} - -export function areaLineX(data, options) { - const {x, y, color, stroke = color, fill = color, z = x === fill || x === stroke ? null : undefined, ...rest} = maybeDenseIntervalY(options); // prettier-ignore - return new AreaLine(data, maybeStackX({...rest, x, y1: y, y2: undefined, z, fill, stroke})); -} - -export function areaLineY(data, options) { - const {x, y, color, stroke = color, fill = color, z = y === fill || y === stroke ? null : undefined, ...rest} = maybeDenseIntervalX(options); // prettier-ignore - return new AreaLine(data, maybeStackY({...rest, x1: x, x2: undefined, y, z, fill, stroke})); -} diff --git a/test/output/aaplClose.svg b/test/output/aaplClose.svg index 11e8483aaa..ab85b87a1f 100644 --- a/test/output/aaplClose.svg +++ b/test/output/aaplClose.svg @@ -66,7 +66,7 @@ 2017 2018 - + diff --git a/test/plots/aapl-close.ts b/test/plots/aapl-close.ts index 0676004373..6c94f732a4 100644 --- a/test/plots/aapl-close.ts +++ b/test/plots/aapl-close.ts @@ -6,7 +6,7 @@ test(async function aaplClose() { const aapl = await d3.csv("data/aapl.csv", d3.autoType); return Plot.plot({ y: {grid: true}, - marks: [Plot.areaLineY(aapl, {x: "Date", y: "Close"}), Plot.ruleY([0])] + marks: [Plot.areaY(aapl, {x: "Date", y: "Close", line: true}), Plot.ruleY([0])] }); }); From d72b9a002e41d0cfbb5e03678a01e0797f42d98f Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 6 Apr 2026 09:13:20 -0400 Subject: [PATCH 7/8] simplify z: null check --- src/marks/area.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/marks/area.js b/src/marks/area.js index 420e8c89b6..902edfcb83 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -135,11 +135,11 @@ export function area(data, options) { } export function areaX(data, options) { - const {x, y, line, color, stroke = color, fill = color, z = x === fill || (line && x === stroke) ? null : undefined, ...rest} = maybeDenseIntervalY(options); // prettier-ignore + const {x, y, line, color, stroke = color, fill = color, z = x === fill || x === stroke ? null : undefined, ...rest} = maybeDenseIntervalY(options); // prettier-ignore return new (line ? AreaLine : Area)(data, maybeStackX({...rest, x, y1: y, y2: undefined, z, stroke, fill})); } export function areaY(data, options) { - const {x, y, line, color, stroke = color, fill = color, z = y === fill || (line && y === stroke) ? null : undefined, ...rest} = maybeDenseIntervalX(options); // prettier-ignore + const {x, y, line, color, stroke = color, fill = color, z = y === fill || y === stroke ? null : undefined, ...rest} = maybeDenseIntervalX(options); // prettier-ignore return new (line ? AreaLine : Area)(data, maybeStackY({...rest, x1: x, x2: undefined, y, z, stroke, fill})); } From 54da7a28e5e1cf6ff870fc0bfadc4711bb37e05b Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 6 Apr 2026 10:21:30 -0400 Subject: [PATCH 8/8] fold color option into AreaOptions --- src/mark.d.ts | 5 ----- src/marks/area.d.ts | 7 +++++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/mark.d.ts b/src/mark.d.ts index 9fc092f45e..43e61da15f 100644 --- a/src/mark.d.ts +++ b/src/mark.d.ts @@ -473,11 +473,6 @@ export interface MarkOptions { channels?: Record; } -export interface ColorOptions { - /** Shorthand for setting both the fill and the stroke. */ - color?: ChannelValueSpec; -} - /** The abstract base class for Mark implementations. */ export class Mark { /** diff --git a/src/marks/area.d.ts b/src/marks/area.d.ts index 9c1dffb748..8eed9953e1 100644 --- a/src/marks/area.d.ts +++ b/src/marks/area.d.ts @@ -1,12 +1,12 @@ import type {ChannelValue, ChannelValueDenseBinSpec, ChannelValueSpec} from "../channel.js"; import type {CurveOptions} from "../curve.js"; -import type {ColorOptions, Data, MarkOptions, RenderableMark} from "../mark.js"; +import type {Data, MarkOptions, RenderableMark} from "../mark.js"; import {MarkerOptions} from "../marker.js"; import type {BinOptions, BinReducer} from "../transforms/bin.js"; import type {StackOptions} from "../transforms/stack.js"; /** Options for the area, areaX, and areaY marks. */ -export interface AreaOptions extends MarkOptions, StackOptions, ColorOptions, CurveOptions { +export interface AreaOptions extends MarkOptions, StackOptions, CurveOptions { /** * The required primary (starting, often left) horizontal position channel, * representing the area’s baseline, typically bound to the *x* scale. For @@ -43,6 +43,9 @@ export interface AreaOptions extends MarkOptions, StackOptions, ColorOptions, Cu * **stroke** if a channel. */ z?: ChannelValue; + + /** Shorthand for setting both the fill and the stroke. */ + color?: ChannelValueSpec; } /** Options for the areaX mark. */