Skip to content

Commit 78db2bb

Browse files
authored
frame channels (#1509)
* frame channels * tider * faithful fill * frame facet channels; update documentation * copy edit
1 parent 4ea4136 commit 78db2bb

File tree

12 files changed

+713
-19
lines changed

12 files changed

+713
-19
lines changed

docs/marks/frame.md

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22

33
import * as Plot from "@observablehq/plot";
44
import * as d3 from "d3";
5-
import {ref} from "vue";
5+
import {ref, shallowRef, onMounted} from "vue";
66
import penguins from "../data/penguins.ts";
77

88
const framed = ref(true);
99

10+
const faithful = shallowRef([]);
11+
12+
onMounted(() => {
13+
d3.tsv("../data/faithful.tsv", d3.autoType).then((data) => (faithful.value = data));
14+
});
15+
1016
</script>
1117

1218
# Frame mark
@@ -50,6 +56,43 @@ Plot.frame({stroke: "red"}).plot({x: {domain: [0, 1], grid: true}})
5056
```
5157
:::
5258

59+
While options are often specified in literal values, such as <span style="border-bottom: solid 2px var(--vp-c-red);">*red*</span> above, the standard [mark channels](../features/marks.md#mark-options) such as **fill** and **stroke** can also be specified as abstract values. For example, in the density heatmap below comparing the delay between eruptions of the Old Faithful geyser (*waiting*) in *x*→ and the duration of the eruption (*eruptions*) in *y*↑, both in minutes, we fill the frame with <span :style="{borderBottom: `solid 2px ${d3.interpolateTurbo(0)}`}">black</span> representing zero density.
60+
61+
:::plot defer
62+
```js
63+
Plot.plot({
64+
inset: 30,
65+
marks: [
66+
Plot.frame({fill: 0}),
67+
Plot.density(faithful, {x: "waiting", y: "eruptions", fill: "density"})
68+
]
69+
})
70+
```
71+
:::
72+
73+
:::tip
74+
This is equivalent to a [rect](./rect.md): `Plot.rect({length: 1}, {fill: 0})`.
75+
:::
76+
77+
You can also place a frame on a specific facet using the **fx** or **fy** option. Below, a frame emphasizes the *Gentoo* facet, say to draw attention to how much bigger they are. 🐧
78+
79+
:::plot
80+
```js
81+
Plot.plot({
82+
marginLeft: 80,
83+
inset: 10,
84+
marks: [
85+
Plot.frame({fy: "Gentoo"}),
86+
Plot.dot(penguins, {x: "body_mass_g", fy: "species"})
87+
]
88+
})
89+
```
90+
:::
91+
92+
:::tip
93+
Or: `Plot.rect({length: 1}, {fy: ["Gentoo"], stroke: "currentColor"})`.
94+
:::
95+
5396
The **anchor** option, if specified to a value of *left*, *right*, *top* or *bottom*, draws only that side of the frame. In that case, the **fill** and **rx**, **ry** options are ignored.
5497

5598
:::plot
@@ -68,7 +111,7 @@ Plot.plot({
68111

69112
## Frame options
70113

71-
The frame mark supports the [standard mark options](../features/marks.md#mark-options), and the **rx** and **ry** options to set the [*x* radius](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/rx) and [*y* radius](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/ry) for rounded corners. It does not accept any data or support channels. The default **stroke** is *currentColor*, and the default **fill** is *none*.
114+
The frame mark supports the [standard mark options](../features/marks.md#mark-options), and the **rx** and **ry** options to set the [*x* radius](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/rx) and [*y* radius](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/ry) for rounded corners. It does not accept any data. The default **stroke** is *currentColor*, and the default **fill** is *none*.
72115

73116
If the **anchor** option is specified as one of *left*, *right*, *top*, or *bottom*, that side is rendered as a single line (and the **fill**, **fillOpacity**, **rx**, and **ry** options are ignored).
74117

src/mark.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import {createChannels, channelDomain, valueObject} from "./channel.js";
1+
import {channelDomain, createChannels, valueObject} from "./channel.js";
22
import {defined} from "./defined.js";
33
import {maybeFacetAnchor} from "./facet.js";
4-
import {arrayify, isDomainSort, isOptions, range} from "./options.js";
5-
import {keyword, maybeNamed} from "./options.js";
4+
import {arrayify, isDomainSort, isOptions, keyword, maybeNamed, range, singleton} from "./options.js";
65
import {maybeProject} from "./projection.js";
76
import {maybeClip, styles} from "./style.js";
87
import {basic, initializer} from "./transforms/basic.js";
@@ -33,8 +32,8 @@ export class Mark {
3332
this.facet = null;
3433
} else {
3534
this.facet = keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude", "super"]);
36-
this.fx = fx;
37-
this.fy = fy;
35+
this.fx = data === singleton && typeof fx === "string" ? [fx] : fx;
36+
this.fy = data === singleton && typeof fy === "string" ? [fy] : fy;
3837
}
3938
this.facetAnchor = maybeFacetAnchor(facetAnchor);
4039
channels = maybeNamed(channels);
@@ -43,10 +42,15 @@ export class Mark {
4342
this.channels = Object.fromEntries(
4443
Object.entries(channels)
4544
.map(([name, channel]) => {
46-
const {value} = channel;
47-
if (isOptions(value)) {
48-
channel = {...channel, value: value.value};
49-
if (value.scale !== undefined) channel.scale = value.scale;
45+
if (isOptions(channel.value)) {
46+
// apply scale overrides
47+
const {value, scale = channel.scale} = channel.value;
48+
channel = {...channel, scale, value};
49+
}
50+
if (data === singleton && typeof channel.value === "string") {
51+
// convert field names to singleton values for decoration marks (e.g., frame)
52+
const {value} = channel;
53+
channel = {...channel, value: [value]};
5054
}
5155
return [name, channel];
5256
})

src/marks/frame.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {create} from "../context.js";
22
import {Mark} from "../mark.js";
3-
import {maybeKeyword, number} from "../options.js";
4-
import {applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";
3+
import {maybeKeyword, number, singleton} from "../options.js";
4+
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";
55

66
const defaults = {
77
ariaLabel: "frame",
@@ -28,7 +28,7 @@ export class Frame extends Mark {
2828
rx,
2929
ry
3030
} = options;
31-
super(undefined, undefined, options, anchor == null ? defaults : lineDefaults);
31+
super(singleton, undefined, options, anchor == null ? defaults : lineDefaults);
3232
this.anchor = maybeKeyword(anchor, "anchor", ["top", "right", "bottom", "left"]);
3333
this.insetTop = number(insetTop);
3434
this.insetRight = number(insetRight);
@@ -45,8 +45,10 @@ export class Frame extends Mark {
4545
const y1 = marginTop + insetTop;
4646
const y2 = height - marginBottom - insetBottom;
4747
return create(anchor ? "svg:line" : "svg:rect", context)
48+
.datum(0)
4849
.call(applyIndirectStyles, this, dimensions, context)
4950
.call(applyDirectStyles, this)
51+
.call(applyChannelStyles, this, channels)
5052
.call(applyTransform, this, {})
5153
.call(
5254
anchor === "left"

src/marks/hexgrid.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {create} from "../context.js";
22
import {Mark} from "../mark.js";
3-
import {number} from "../options.js";
4-
import {applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js";
3+
import {number, singleton} from "../options.js";
4+
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js";
55
import {sqrt4_3} from "../symbol.js";
66
import {ox, oy} from "../transforms/hexbin.js";
77

@@ -18,7 +18,7 @@ export function hexgrid(options) {
1818

1919
export class Hexgrid extends Mark {
2020
constructor({binWidth = 20, clip = true, ...options} = {}) {
21-
super(undefined, undefined, {clip, ...options}, defaults);
21+
super(singleton, undefined, {clip, ...options}, defaults);
2222
this.binWidth = number(binWidth);
2323
}
2424
render(index, scales, channels, dimensions, context) {
@@ -45,9 +45,10 @@ export class Hexgrid extends Mark {
4545
}
4646
}
4747
return create("svg:g", context)
48+
.datum(0)
4849
.call(applyIndirectStyles, this, dimensions, context)
4950
.call(applyTransform, this, {}, offset + ox, offset + oy)
50-
.call((g) => g.append("path").call(applyDirectStyles, this).attr("d", d))
51+
.call((g) => g.append("path").call(applyDirectStyles, this).call(applyChannelStyles, this, channels).attr("d", d))
5152
.node();
5253
}
5354
}

src/options.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ function floater(f) {
3838
return (d, i) => coerceNumber(f(d, i));
3939
}
4040

41+
export const singleton = [null]; // for data-less decoration marks, e.g. frame
4142
export const field = (name) => (d) => d[name];
4243
export const indexOf = {transform: range};
4344
export const identity = {transform: (d) => d};

test/marks/frame-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import assert from "assert";
33

44
it("frame(options) has the expected defaults", () => {
55
const frame = Plot.frame();
6-
assert.strictEqual(frame.data, undefined);
6+
assert.deepStrictEqual(frame.data, [null]);
77
assert.strictEqual(frame.transform, undefined);
88
assert.deepStrictEqual(frame.channels, {});
99
assert.strictEqual(frame.fill, "none");

test/output/faithfulDensityFill.svg

Lines changed: 76 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)