diff --git a/docs/marks/area.md b/docs/marks/area.md
index e967127b5d..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 mark is often paired with a [line](./line.md) and [rule](./rule.md) mark to accentuate the topline and 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,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])
]
})
@@ -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])
]
})
@@ -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])
]
})
@@ -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])
]
})
@@ -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 is true, the **stroke** applies exclusively to the topline, and the **fillOpacity** defaults to 0.3.
## areaX(*data*, *options*) {#areaX}
@@ -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 is true, the **stroke** applies exclusively to the topline, and the **fillOpacity** defaults to 0.3.
+
## area(*data*, *options*) {#area}
```js
diff --git a/src/marks/area.d.ts b/src/marks/area.d.ts
index 49aa3011be..8eed9953e1 100644
--- a/src/marks/area.d.ts
+++ b/src/marks/area.d.ts
@@ -1,6 +1,7 @@
import type {ChannelValue, ChannelValueDenseBinSpec, ChannelValueSpec} from "../channel.js";
import type {CurveOptions} from "../curve.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";
@@ -42,6 +43,9 @@ export interface AreaOptions extends MarkOptions, StackOptions, CurveOptions {
* **stroke** if a channel.
*/
z?: ChannelValue;
+
+ /** Shorthand for setting both the fill and the stroke. */
+ color?: ChannelValueSpec;
}
/** Options for the areaX mark. */
@@ -124,6 +128,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 +170,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 +202,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 f820302a27..902edfcb83 100644
--- a/src/marks/area.js
+++ b/src/marks/area.js
@@ -1,14 +1,15 @@
-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",
@@ -16,8 +17,18 @@ const defaults = {
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,
@@ -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}));
}
diff --git a/test/output/aaplClose.svg b/test/output/aaplClose.svg
index 5fb51a1e86..ab85b87a1f 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..6c94f732a4 100644
--- a/test/plots/aapl-close.ts
+++ b/test/plots/aapl-close.ts
@@ -6,11 +6,7 @@ test(async function aaplClose() {
const aapl = await d3.csv("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])]
});
});