Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
18 changes: 8 additions & 10 deletions docs/marks/area.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 **line** option <VersionBadge pr="2407" /> 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
Expand All @@ -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.areaY(aapl, {x: "Date", y: "Close", line: true}),
Plot.ruleY([0])
]
})
Expand Down Expand Up @@ -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.areaY(aapl, {x: "Date", y: "Close", line: true}),
Plot.ruleY([0])
]
})
Expand All @@ -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.areaX(aapl, {y: "Date", x: "Close", line: true}),
Plot.ruleX([0])
]
})
Expand All @@ -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.areaY(aapl, {x: "Date", y: (d) => d.Date.getUTCMonth() < 3 ? NaN : d.Close, line: true}),
Plot.ruleY([0])
]
})
Expand Down Expand Up @@ -308,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 <VersionBadge pr="2407" /> is true, the **stroke** applies exclusively to the topline, and the **fillOpacity** defaults to 0.3.

## areaX(*data*, *options*) {#areaX}

Expand All @@ -326,6 +322,8 @@ 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.

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 <VersionBadge pr="2407" /> is true, the **stroke** applies exclusively to the topline, and the **fillOpacity** defaults to 0.3.

## area(*data*, *options*) {#area}

```js
Expand Down
5 changes: 5 additions & 0 deletions src/mark.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,11 @@ export interface MarkOptions {
channels?: Record<string, Channel | ChannelValue>;
}

export interface ColorOptions {
/** Shorthand for setting both the fill and the stroke. */
color?: ChannelValueSpec;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be supported on all marks…

Copy link
Copy Markdown
Contributor

@Fil Fil Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once we support color, we'll often want to use this over stroke or fill, because it's less cognitive work—so it should "just work" everywhere.

I feel it can be a bit tricky though, because it might not make sense for all marks to set both stroke and fill. For instance, I don't think we'd want to set a fill on a line (it's rarely a good idea), or a link (it doesn't apply, so better stick with fill: "none"), but maybe on an arrow (it can apply if there is a non-zero bend), although I would rarely do it.

In this sense "color" should mean "the base color for the mark" rather than "a shorthand for stroke and fill".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the simpler definition of shorthand is easier to understand (and worse is better) and we shouldn’t try to define a “base color” separately for each mark.

I can remove the color shorthand from this PR if you think it’s a bad idea and we can discuss it later.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please remove it, I think it might set the wrong expectation

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it’s worth we already support a color option on the following marks: axis, grid, auto, crosshair, bollinger. The definition I am using here feels consistent with that existing usage.

}

/** The abstract base class for Mark implementations. */
export class Mark {
/**
Expand Down
12 changes: 8 additions & 4 deletions src/marks/area.d.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -124,6 +125,9 @@ export interface AreaYOptions extends Omit<AreaOptions, "x1" | "x2">, 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {}
80 changes: 72 additions & 8 deletions src/marks/area.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
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";

const defaults = {
const areaDefaults = {
ariaLabel: "area",
strokeWidth: 1,
strokeLinecap: "round",
strokeLinejoin: "round",
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 = {}) {
constructor(data, options = {}, defaults = areaDefaults) {
const {x1, y1, x2, y2, z, curve, tension} = options;
super(
data,
Expand Down Expand Up @@ -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 || 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 || 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}));
}
10 changes: 5 additions & 5 deletions test/output/aaplClose.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 1 addition & 5 deletions test/plots/aapl-close.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@ test(async function aaplClose() {
const aapl = await d3.csv<any>("data/aapl.csv", d3.autoType);
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.ruleY([0])
]
marks: [Plot.areaY(aapl, {x: "Date", y: "Close", line: true}), Plot.ruleY([0])]
});
});

Expand Down