Skip to content

Commit 8ef1e7f

Browse files
mbostockFil
andauthored
document option (#969)
* document option * Context(options) * document document * Update README Co-authored-by: Philippe Rivière <[email protected]>
1 parent f0d619e commit 8ef1e7f

29 files changed

+250
-146
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ These options determine the overall layout of the plot; all are specified as num
8888
* **margin** - shorthand for the four margins
8989
* **width** - the outer width of the plot (including margins)
9090
* **height** - the outer height of the plot (including margins)
91+
* **document** - the [document](https://developer.mozilla.org/en-US/docs/Web/API/Document) used to create plot elements; defaults to window.document
9192

9293
The default **width** is 640. On Observable, the width can be set to the [standard width](https://github.com/observablehq/stdlib/blob/main/README.md#width) to make responsive plots. The default **height** is chosen automatically based on the plot’s associated scales; for example, if *y* is linear and there is no *fy* scale, it might be 396.
9394

@@ -295,7 +296,7 @@ Plot automatically generates axes for position scales. You can configure these a
295296
* *scale*.**label** - a string to label the axis
296297
* *scale*.**labelAnchor** - the label anchor: *top*, *right*, *bottom*, *left*, or *center*
297298
* *scale*.**labelOffset** - the label position offset (in pixels; default 0, typically for facet axes)
298-
* *scale*.**fontVariant** - the font-variant attribute for axis ticks; defaults to tabular-nums for quantitative axes.
299+
* *scale*.**fontVariant** - the font-variant attribute for axis ticks; defaults to tabular-nums for quantitative axes
299300
* *scale*.**ariaLabel** - a short label representing the axis in the accessibility tree
300301
* *scale*.**ariaDescription** - a textual description for the axis
301302

src/axis.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import {axisTop, axisBottom, axisRight, axisLeft, create, format, utcFormat} from "d3";
2-
import {boolean, take, number, string, keyword, maybeKeyword, constant, isTemporal} from "./options.js";
1+
import {axisTop, axisBottom, axisRight, axisLeft, format, utcFormat} from "d3";
2+
import {create} from "./context.js";
33
import {formatIsoDate} from "./format.js";
44
import {radians} from "./math.js";
5+
import {boolean, take, number, string, keyword, maybeKeyword, constant, isTemporal} from "./options.js";
56
import {applyAttr, impliedString} from "./style.js";
67

78
export class AxisX {
@@ -53,7 +54,8 @@ export class AxisX {
5354
facetMarginBottom,
5455
labelMarginLeft = 0,
5556
labelMarginRight = 0
56-
}
57+
},
58+
context
5759
) {
5860
const {
5961
axis,
@@ -69,7 +71,7 @@ export class AxisX {
6971
const offset = name === "x" ? 0 : axis === "top" ? marginTop - facetMarginTop : marginBottom - facetMarginBottom;
7072
const offsetSign = axis === "top" ? -1 : 1;
7173
const ty = offsetSign * offset + (axis === "top" ? marginTop : height - marginBottom);
72-
return create("svg:g")
74+
return create("svg:g", context)
7375
.call(applyAria, this)
7476
.attr("transform", `translate(${offsetLeft},${ty})`)
7577
.call(createAxis(axis === "top" ? axisTop : axisBottom, x, this))
@@ -144,7 +146,8 @@ export class AxisY {
144146
offsetTop = 0,
145147
facetMarginLeft,
146148
facetMarginRight
147-
}
149+
},
150+
context
148151
) {
149152
const {
150153
axis,
@@ -160,7 +163,7 @@ export class AxisY {
160163
const offset = name === "y" ? 0 : axis === "left" ? marginLeft - facetMarginLeft : marginRight - facetMarginRight;
161164
const offsetSign = axis === "left" ? -1 : 1;
162165
const tx = offsetSign * offset + (axis === "right" ? width - marginRight : marginLeft);
163-
return create("svg:g")
166+
return create("svg:g", context)
164167
.call(applyAria, this)
165168
.attr("transform", `translate(${tx},${offsetTop})`)
166169
.call(createAxis(axis === "right" ? axisRight : axisLeft, y, this))

src/context.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {creator, select} from "d3";
2+
3+
export function Context({document = window.document} = {}) {
4+
return {document};
5+
}
6+
7+
export function create(name, {document}) {
8+
return select(creator(name).call(document.documentElement));
9+
}

src/legends.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import {rgb} from "d3";
2-
import {isScaleOptions} from "./options.js";
3-
import {normalizeScale} from "./scales.js";
2+
import {Context} from "./context.js";
43
import {legendRamp} from "./legends/ramp.js";
54
import {legendSwatches, legendSymbols} from "./legends/swatches.js";
5+
import {inherit, isScaleOptions} from "./options.js";
6+
import {normalizeScale} from "./scales.js";
67

78
const legendRegistry = new Map([
89
["symbol", legendSymbols],
@@ -14,6 +15,7 @@ export function legend(options = {}) {
1415
for (const [key, value] of legendRegistry) {
1516
const scale = options[key];
1617
if (isScaleOptions(scale)) { // e.g., ignore {color: "red"}
18+
const context = Context(options);
1719
let hint;
1820
// For symbol legends, pass a hint to the symbol scale.
1921
if (key === "symbol") {
@@ -22,24 +24,24 @@ export function legend(options = {}) {
2224
}
2325
return value(
2426
normalizeScale(key, scale, hint),
25-
legendOptions(scale, options),
27+
legendOptions(context, scale, options),
2628
key => isScaleOptions(options[key]) ? normalizeScale(key, options[key]) : null
2729
);
2830
}
2931
}
3032
throw new Error("unknown legend type; no scale found");
3133
}
3234

33-
export function exposeLegends(scales, defaults = {}) {
35+
export function exposeLegends(scales, context, defaults = {}) {
3436
return (key, options) => {
3537
if (!legendRegistry.has(key)) throw new Error(`unknown legend type: ${key}`);
3638
if (!(key in scales)) return;
37-
return legendRegistry.get(key)(scales[key], legendOptions(defaults[key], options), key => scales[key]);
39+
return legendRegistry.get(key)(scales[key], legendOptions(context, defaults[key], options), key => scales[key]);
3840
};
3941
}
4042

41-
function legendOptions({label, ticks, tickFormat} = {}, options = {}) {
42-
return {label, ticks, tickFormat, ...options};
43+
function legendOptions(context, {label, ticks, tickFormat} = {}, options) {
44+
return inherit(options, context, {label, ticks, tickFormat});
4345
}
4446

4547
function legendColor(color, {
@@ -71,12 +73,12 @@ function interpolateOpacity(color) {
7173
return t => `rgba(${r},${g},${b},${t})`;
7274
}
7375

74-
export function Legends(scales, options) {
76+
export function Legends(scales, context, options) {
7577
const legends = [];
7678
for (const [key, value] of legendRegistry) {
7779
const o = options[key];
7880
if (o?.legend && (key in scales)) {
79-
const legend = value(scales[key], legendOptions(scales[key], o), key => scales[key]);
81+
const legend = value(scales[key], legendOptions(context, scales[key], o), key => scales[key]);
8082
if (legend != null) legends.push(legend);
8183
}
8284
}

src/legends/ramp.js

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,32 @@
1-
import {create, quantize, interpolateNumber, piecewise, format, scaleBand, scaleLinear, axisBottom} from "d3";
1+
import {quantize, interpolateNumber, piecewise, format, scaleBand, scaleLinear, axisBottom} from "d3";
22
import {inferFontVariant} from "../axes.js";
3+
import {Context, create} from "../context.js";
34
import {map} from "../options.js";
45
import {interpolatePiecewise} from "../scales/quantitative.js";
56
import {applyInlineStyles, impliedString, maybeClassName} from "../style.js";
67

7-
export function legendRamp(color, {
8-
label = color.label,
9-
tickSize = 6,
10-
width = 240,
11-
height = 44 + tickSize,
12-
marginTop = 18,
13-
marginRight = 0,
14-
marginBottom = 16 + tickSize,
15-
marginLeft = 0,
16-
style,
17-
ticks = (width - marginLeft - marginRight) / 64,
18-
tickFormat,
19-
fontVariant = inferFontVariant(color),
20-
round = true,
21-
className
22-
}) {
8+
export function legendRamp(color, options) {
9+
let {
10+
label = color.label,
11+
tickSize = 6,
12+
width = 240,
13+
height = 44 + tickSize,
14+
marginTop = 18,
15+
marginRight = 0,
16+
marginBottom = 16 + tickSize,
17+
marginLeft = 0,
18+
style,
19+
ticks = (width - marginLeft - marginRight) / 64,
20+
tickFormat,
21+
fontVariant = inferFontVariant(color),
22+
round = true,
23+
className
24+
} = options;
25+
const context = Context(options);
2326
className = maybeClassName(className);
2427
if (tickFormat === null) tickFormat = () => null;
2528

26-
const svg = create("svg")
29+
const svg = create("svg", context)
2730
.attr("class", className)
2831
.attr("font-family", "system-ui, sans-serif")
2932
.attr("font-size", 10)
@@ -83,13 +86,24 @@ export function legendRamp(color, {
8386
)
8487
);
8588

89+
// Construct a 256×1 canvas, filling each pixel using the interpolator.
90+
const n = 256;
91+
const canvas = context.document.createElement("canvas");
92+
canvas.width = n;
93+
canvas.height = 1;
94+
const context2 = canvas.getContext("2d");
95+
for (let i = 0, j = n - 1; i < n; ++i) {
96+
context2.fillStyle = interpolator(i / j);
97+
context2.fillRect(i, 0, 1, 1);
98+
}
99+
86100
svg.append("image")
87101
.attr("x", marginLeft)
88102
.attr("y", marginTop)
89103
.attr("width", width - marginLeft - marginRight)
90104
.attr("height", height - marginTop - marginBottom)
91105
.attr("preserveAspectRatio", "none")
92-
.attr("xlink:href", ramp(interpolator).toDataURL());
106+
.attr("xlink:href", canvas.toDataURL());
93107
}
94108

95109
// Threshold
@@ -162,13 +176,3 @@ export function legendRamp(color, {
162176

163177
return svg.node();
164178
}
165-
166-
function ramp(color, n = 256) {
167-
const canvas = create("canvas").attr("width", n).attr("height", 1).node();
168-
const context = canvas.getContext("2d");
169-
for (let i = 0; i < n; ++i) {
170-
context.fillStyle = color(i / (n - 1));
171-
context.fillRect(i, 0, 1, 1);
172-
}
173-
return canvas;
174-
}

src/legends/swatches.js

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import {create, path} from "d3";
1+
import {path} from "d3";
22
import {inferFontVariant} from "../axes.js";
33
import {maybeAutoTickFormat} from "../axis.js";
4+
import {Context, create} from "../context.js";
45
import {isNoneish, maybeColorChannel, maybeNumberChannel} from "../options.js";
56
import {isOrdinalScale} from "../scales.js";
67
import {applyInlineStyles, impliedString, maybeClassName} from "../style.js";
@@ -74,23 +75,25 @@ export function legendSymbols(symbol, {
7475
);
7576
}
7677

77-
function legendItems(scale, {
78-
columns,
79-
tickFormat,
80-
fontVariant = inferFontVariant(scale),
81-
// TODO label,
82-
swatchSize = 15,
83-
swatchWidth = swatchSize,
84-
swatchHeight = swatchSize,
85-
marginLeft = 0,
86-
className,
87-
style,
88-
width
89-
} = {}, swatch, swatchStyle) {
78+
function legendItems(scale, options = {}, swatch, swatchStyle) {
79+
let {
80+
columns,
81+
tickFormat,
82+
fontVariant = inferFontVariant(scale),
83+
// TODO label,
84+
swatchSize = 15,
85+
swatchWidth = swatchSize,
86+
swatchHeight = swatchSize,
87+
marginLeft = 0,
88+
className,
89+
style,
90+
width
91+
} = options;
92+
const context = Context(options);
9093
className = maybeClassName(className);
9194
tickFormat = maybeAutoTickFormat(tickFormat, scale.domain);
9295

93-
const swatches = create("div")
96+
const swatches = create("div", context)
9497
.attr("class", className)
9598
.attr("style", `
9699
--swatchWidth: ${+swatchWidth}px;
@@ -152,7 +155,7 @@ function legendItems(scale, {
152155
.attr("class", `${className}-swatch`)
153156
.call(swatch, scale)
154157
.append(function() {
155-
return document.createTextNode(tickFormat.apply(this, arguments));
158+
return this.ownerDocument.createTextNode(tickFormat.apply(this, arguments));
156159
});
157160
}
158161

src/marks/area.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import {area as shapeArea, create} from "d3";
1+
import {area as shapeArea} from "d3";
2+
import {create} from "../context.js";
23
import {Curve} from "../curve.js";
3-
import {Mark} from "../plot.js";
44
import {first, indexOf, maybeZ, second} from "../options.js";
5+
import {Mark} from "../plot.js";
56
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, groupIndex} from "../style.js";
67
import {maybeDenseIntervalX, maybeDenseIntervalY} from "../transforms/bin.js";
78
import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js";
@@ -36,9 +37,9 @@ export class Area extends Mark {
3637
filter(index) {
3738
return index;
3839
}
39-
render(index, scales, channels, dimensions) {
40+
render(index, scales, channels, dimensions, context) {
4041
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels;
41-
return create("svg:g")
42+
return create("svg:g", context)
4243
.call(applyIndirectStyles, this, scales, dimensions)
4344
.call(applyTransform, this, scales, 0, 0)
4445
.call(g => g.selectAll()

src/marks/arrow.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import {create} from "d3";
1+
import {create} from "../context.js";
22
import {radians} from "../math.js";
3+
import {constant} from "../options.js";
34
import {Mark} from "../plot.js";
45
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";
56
import {maybeSameValue} from "./link.js";
6-
import {constant} from "../options.js";
77

88
const defaults = {
99
ariaLabel: "arrow",
@@ -45,7 +45,7 @@ export class Arrow extends Mark {
4545
this.insetStart = +insetStart;
4646
this.insetEnd = +insetEnd;
4747
}
48-
render(index, scales, channels, dimensions) {
48+
render(index, scales, channels, dimensions, context) {
4949
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, SW} = channels;
5050
const {strokeWidth, bend, headAngle, headLength, insetStart, insetEnd} = this;
5151
const sw = SW ? i => SW[i] : constant(strokeWidth === undefined ? 1 : strokeWidth);
@@ -65,7 +65,7 @@ export class Arrow extends Mark {
6565
// the end point) relative to the stroke width.
6666
const wingScale = headLength / 1.5;
6767

68-
return create("svg:g")
68+
return create("svg:g", context)
6969
.call(applyIndirectStyles, this, scales, dimensions)
7070
.call(applyTransform, this, scales)
7171
.call(g => g.selectAll()

src/marks/bar.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {create} from "d3";
2-
import {Mark} from "../plot.js";
1+
import {create} from "../context.js";
32
import {identity, indexOf, number} from "../options.js";
3+
import {Mark} from "../plot.js";
44
import {isCollapsed} from "../scales.js";
55
import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js";
66
import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js";
@@ -18,9 +18,9 @@ export class AbstractBar extends Mark {
1818
this.rx = impliedString(rx, "auto"); // number or percentage
1919
this.ry = impliedString(ry, "auto");
2020
}
21-
render(index, scales, channels, dimensions) {
21+
render(index, scales, channels, dimensions, context) {
2222
const {rx, ry} = this;
23-
return create("svg:g")
23+
return create("svg:g", context)
2424
.call(applyIndirectStyles, this, scales, dimensions)
2525
.call(this._transform, this, scales)
2626
.call(g => g.selectAll()

0 commit comments

Comments
 (0)