Skip to content

Commit cf72691

Browse files
authored
arealine mark (#2407)
* arealine * color option shorthand * fix line offset * document area line mark * fix docs:build? * restore line option; fillOpacity 0.3 * simplify z: null check * fold color option into AreaOptions
1 parent 5a84c28 commit cf72691

5 files changed

Lines changed: 95 additions & 30 deletions

File tree

docs/marks/area.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Plot.areaY(aapl, {x: "Date", y: "Close"}).plot()
2020

2121
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.
2222

23-
The area mark is often paired with a [line](./line.md) and [rule](./rule.md) mark to accentuate the topline and baseline.
23+
The **line** option <VersionBadge pr="2407" /> strokes the topline. It is often paired with a [rule](./rule.md) mark to denote the baseline.
2424

2525
:::plot https://observablehq.com/@observablehq/plot-area-and-line
2626
```js
@@ -29,8 +29,7 @@ Plot.plot({
2929
grid: true
3030
},
3131
marks: [
32-
Plot.areaY(aapl, {x: "Date", y: "Close", fillOpacity: 0.3}),
33-
Plot.lineY(aapl, {x: "Date", y: "Close"}),
32+
Plot.areaY(aapl, {x: "Date", y: "Close", line: true}),
3433
Plot.ruleY([0])
3534
]
3635
})
@@ -94,8 +93,7 @@ Plot.plot({
9493
reverse: true
9594
},
9695
marks: [
97-
Plot.areaY(aapl, {x: "Date", y: "Close", fillOpacity: 0.3}),
98-
Plot.lineY(aapl, {x: "Date", y: "Close"}),
96+
Plot.areaY(aapl, {x: "Date", y: "Close", line: true}),
9997
Plot.ruleY([0])
10098
]
10199
})
@@ -111,8 +109,7 @@ Plot.plot({
111109
grid: true
112110
},
113111
marks: [
114-
Plot.areaX(aapl, {y: "Date", x: "Close", fillOpacity: 0.3}),
115-
Plot.lineX(aapl, {y: "Date", x: "Close"}),
112+
Plot.areaX(aapl, {y: "Date", x: "Close", line: true}),
116113
Plot.ruleX([0])
117114
]
118115
})
@@ -128,8 +125,7 @@ Plot.plot({
128125
grid: true
129126
},
130127
marks: [
131-
Plot.areaY(aapl, {x: "Date", y: (d) => d.Date.getUTCMonth() < 3 ? NaN : d.Close, fillOpacity: 0.3}),
132-
Plot.lineY(aapl, {x: "Date", y: (d) => d.Date.getUTCMonth() < 3 ? NaN : d.Close}),
128+
Plot.areaY(aapl, {x: "Date", y: (d) => d.Date.getUTCMonth() < 3 ? NaN : d.Close, line: true}),
133129
Plot.ruleY([0])
134130
]
135131
})
@@ -308,7 +304,7 @@ Plot.areaY(observations, {x: "date", y: "temperature", interval: "day"})
308304

309305
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.
310306

311-
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*.
307+
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.
312308

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

@@ -326,6 +322,8 @@ Plot.areaX(observations, {y: "date", x: "temperature", interval: "day"})
326322

327323
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.
328324

325+
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.
326+
329327
## area(*data*, *options*) {#area}
330328

331329
```js

src/marks/area.d.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {ChannelValue, ChannelValueDenseBinSpec, ChannelValueSpec} from "../channel.js";
22
import type {CurveOptions} from "../curve.js";
33
import type {Data, MarkOptions, RenderableMark} from "../mark.js";
4+
import {MarkerOptions} from "../marker.js";
45
import type {BinOptions, BinReducer} from "../transforms/bin.js";
56
import type {StackOptions} from "../transforms/stack.js";
67

@@ -42,6 +43,9 @@ export interface AreaOptions extends MarkOptions, StackOptions, CurveOptions {
4243
* **stroke** if a channel.
4344
*/
4445
z?: ChannelValue;
46+
47+
/** Shorthand for setting both the fill and the stroke. */
48+
color?: ChannelValueSpec;
4549
}
4650

4751
/** Options for the areaX mark. */
@@ -124,6 +128,9 @@ export interface AreaYOptions extends Omit<AreaOptions, "x1" | "x2">, BinOptions
124128
reduce?: BinReducer;
125129
}
126130

131+
/** The area mark’s line option. */
132+
export type AreaLineOptions = {line?: false} | ({line: true} & MarkerOptions);
133+
127134
/**
128135
* Returns a new area mark with the given *data* and *options*. The area mark is
129136
* 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;
163170
* channels. When any of these channels are used, setting an explicit **z**
164171
* channel (possibly to null) is strongly recommended.
165172
*/
166-
export function areaX(data?: Data, options?: AreaXOptions): Area;
173+
export function areaX(data?: Data, options?: AreaXOptions & AreaLineOptions): Area;
167174

168175
/**
169176
* Returns a new horizontally-oriented area mark for the given *data* and
@@ -195,7 +202,7 @@ export function areaX(data?: Data, options?: AreaXOptions): Area;
195202
* channels. When any of these channels are used, setting an explicit **z**
196203
* channel (possibly to null) is strongly recommended.
197204
*/
198-
export function areaY(data?: Data, options?: AreaYOptions): Area;
205+
export function areaY(data?: Data, options?: AreaYOptions & AreaLineOptions): Area;
199206

200207
/** The area mark. */
201208
export class Area extends RenderableMark {}

src/marks/area.js

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,34 @@
1-
import {area as shapeArea} from "d3";
1+
import {area as shapeArea, line as shapeLine} from "d3";
22
import {create} from "../context.js";
33
import {maybeCurve} from "../curve.js";
44
import {Mark} from "../mark.js";
5+
import {applyGroupedMarkers, markers} from "../marker.js";
56
import {first, maybeZ, second} from "../options.js";
67
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js";
7-
import {groupIndex} from "../style.js";
8+
import {groupIndex, offset} from "../style.js";
89
import {maybeDenseIntervalX, maybeDenseIntervalY} from "../transforms/bin.js";
910
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
1011

11-
const defaults = {
12+
const areaDefaults = {
1213
ariaLabel: "area",
1314
strokeWidth: 1,
1415
strokeLinecap: "round",
1516
strokeLinejoin: "round",
1617
strokeMiterlimit: 1
1718
};
1819

20+
const areaLineDefaults = {
21+
ariaLabel: "area-line",
22+
fillOpacity: 0.3,
23+
stroke: "currentColor",
24+
strokeWidth: 1.5,
25+
strokeLinecap: "round",
26+
strokeLinejoin: "round",
27+
strokeMiterlimit: 1
28+
};
29+
1930
export class Area extends Mark {
20-
constructor(data, options = {}) {
31+
constructor(data, options = {}, defaults = areaDefaults) {
2132
const {x1, y1, x2, y2, z, curve, tension} = options;
2233
super(
2334
data,
@@ -65,17 +76,70 @@ export class Area extends Mark {
6576
}
6677
}
6778

79+
class AreaLine extends Area {
80+
constructor(data, options = {}) {
81+
super(data, options, areaLineDefaults);
82+
markers(this, options);
83+
}
84+
render(index, scales, channels, dimensions, context) {
85+
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels;
86+
return create("svg:g", context)
87+
.call(applyIndirectStyles, this, dimensions, context)
88+
.call(applyTransform, this, scales, 0, 0)
89+
.call((g) =>
90+
g
91+
.selectAll()
92+
.data(groupIndex(index, [X1, Y1, X2, Y2], this, channels))
93+
.enter()
94+
.append("g")
95+
.call(applyDirectStyles, this)
96+
.call(applyGroupedChannelStyles, this, channels)
97+
.call((e) =>
98+
e
99+
.append("path")
100+
.attr("stroke", "none")
101+
.attr(
102+
"d",
103+
shapeArea()
104+
.curve(this.curve)
105+
.defined((i) => i >= 0)
106+
.x0((i) => X1[i])
107+
.y0((i) => Y1[i])
108+
.x1((i) => X2[i])
109+
.y1((i) => Y2[i])
110+
)
111+
)
112+
.call((e) =>
113+
e
114+
.append("path")
115+
.call(applyGroupedMarkers, this, channels, context)
116+
.attr("fill", "none")
117+
.attr("transform", offset ? `translate(${offset},${offset})` : null)
118+
.attr(
119+
"d",
120+
shapeLine()
121+
.curve(this.curve)
122+
.defined((i) => i >= 0)
123+
.x((i) => X2[i])
124+
.y((i) => Y2[i])
125+
)
126+
)
127+
)
128+
.node();
129+
}
130+
}
131+
68132
export function area(data, options) {
69133
if (options === undefined) return areaY(data, {x: first, y: second});
70134
return new Area(data, options);
71135
}
72136

73137
export function areaX(data, options) {
74-
const {x, y, fill, z = x === fill ? null : undefined, ...rest} = maybeDenseIntervalY(options);
75-
return new Area(data, maybeStackX({...rest, x, y1: y, y2: undefined, z, fill}));
138+
const {x, y, line, color, stroke = color, fill = color, z = x === fill || x === stroke ? null : undefined, ...rest} = maybeDenseIntervalY(options); // prettier-ignore
139+
return new (line ? AreaLine : Area)(data, maybeStackX({...rest, x, y1: y, y2: undefined, z, stroke, fill}));
76140
}
77141

78142
export function areaY(data, options) {
79-
const {x, y, fill, z = y === fill ? null : undefined, ...rest} = maybeDenseIntervalX(options);
80-
return new Area(data, maybeStackY({...rest, x1: x, x2: undefined, y, z, fill}));
143+
const {x, y, line, color, stroke = color, fill = color, z = y === fill || y === stroke ? null : undefined, ...rest} = maybeDenseIntervalX(options); // prettier-ignore
144+
return new (line ? AreaLine : Area)(data, maybeStackY({...rest, x1: x, x2: undefined, y, z, stroke, fill}));
81145
}

test/output/aaplClose.svg

Lines changed: 5 additions & 5 deletions
Loading

test/plots/aapl-close.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@ test(async function aaplClose() {
66
const aapl = await d3.csv<any>("data/aapl.csv", d3.autoType);
77
return Plot.plot({
88
y: {grid: true},
9-
marks: [
10-
Plot.areaY(aapl, {x: "Date", y: "Close", fillOpacity: 0.1}),
11-
Plot.lineY(aapl, {x: "Date", y: "Close"}),
12-
Plot.ruleY([0])
13-
]
9+
marks: [Plot.areaY(aapl, {x: "Date", y: "Close", line: true}), Plot.ruleY([0])]
1410
});
1511
});
1612

0 commit comments

Comments
 (0)