Skip to content

document option #969

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 4, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ These options determine the overall layout of the plot; all are specified as num
* **margin** - shorthand for the four margins
* **width** - the outer width of the plot (including margins)
* **height** - the outer height of the plot (including margins)
* **document** - the [document](https://developer.mozilla.org/en-US/docs/Web/API/Document) used to create plot elements; defaults to window.document

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.

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

Expand Down
15 changes: 9 additions & 6 deletions src/axis.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {axisTop, axisBottom, axisRight, axisLeft, create, format, utcFormat} from "d3";
import {boolean, take, number, string, keyword, maybeKeyword, constant, isTemporal} from "./options.js";
import {axisTop, axisBottom, axisRight, axisLeft, format, utcFormat} from "d3";
import {create} from "./context.js";
import {formatIsoDate} from "./format.js";
import {radians} from "./math.js";
import {boolean, take, number, string, keyword, maybeKeyword, constant, isTemporal} from "./options.js";
import {applyAttr, impliedString} from "./style.js";

export class AxisX {
Expand Down Expand Up @@ -53,7 +54,8 @@ export class AxisX {
facetMarginBottom,
labelMarginLeft = 0,
labelMarginRight = 0
}
},
context
) {
const {
axis,
Expand All @@ -69,7 +71,7 @@ export class AxisX {
const offset = name === "x" ? 0 : axis === "top" ? marginTop - facetMarginTop : marginBottom - facetMarginBottom;
const offsetSign = axis === "top" ? -1 : 1;
const ty = offsetSign * offset + (axis === "top" ? marginTop : height - marginBottom);
return create("svg:g")
return create("svg:g", context)
.call(applyAria, this)
.attr("transform", `translate(${offsetLeft},${ty})`)
.call(createAxis(axis === "top" ? axisTop : axisBottom, x, this))
Expand Down Expand Up @@ -144,7 +146,8 @@ export class AxisY {
offsetTop = 0,
facetMarginLeft,
facetMarginRight
}
},
context
) {
const {
axis,
Expand All @@ -160,7 +163,7 @@ export class AxisY {
const offset = name === "y" ? 0 : axis === "left" ? marginLeft - facetMarginLeft : marginRight - facetMarginRight;
const offsetSign = axis === "left" ? -1 : 1;
const tx = offsetSign * offset + (axis === "right" ? width - marginRight : marginLeft);
return create("svg:g")
return create("svg:g", context)
.call(applyAria, this)
.attr("transform", `translate(${tx},${offsetTop})`)
.call(createAxis(axis === "right" ? axisRight : axisLeft, y, this))
Expand Down
9 changes: 9 additions & 0 deletions src/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {creator, select} from "d3";

export function Context({document = window.document} = {}) {
return {document};
}

export function create(name, {document}) {
return select(creator(name).call(document.documentElement));
}
20 changes: 11 additions & 9 deletions src/legends.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {rgb} from "d3";
import {isScaleOptions} from "./options.js";
import {normalizeScale} from "./scales.js";
import {Context} from "./context.js";
import {legendRamp} from "./legends/ramp.js";
import {legendSwatches, legendSymbols} from "./legends/swatches.js";
import {inherit, isScaleOptions} from "./options.js";
import {normalizeScale} from "./scales.js";

const legendRegistry = new Map([
["symbol", legendSymbols],
Expand All @@ -14,6 +15,7 @@ export function legend(options = {}) {
for (const [key, value] of legendRegistry) {
const scale = options[key];
if (isScaleOptions(scale)) { // e.g., ignore {color: "red"}
const context = Context(options);
let hint;
// For symbol legends, pass a hint to the symbol scale.
if (key === "symbol") {
Expand All @@ -22,24 +24,24 @@ export function legend(options = {}) {
}
return value(
normalizeScale(key, scale, hint),
legendOptions(scale, options),
legendOptions(context, scale, options),
key => isScaleOptions(options[key]) ? normalizeScale(key, options[key]) : null
);
}
}
throw new Error("unknown legend type; no scale found");
}

export function exposeLegends(scales, defaults = {}) {
export function exposeLegends(scales, context, defaults = {}) {
return (key, options) => {
if (!legendRegistry.has(key)) throw new Error(`unknown legend type: ${key}`);
if (!(key in scales)) return;
return legendRegistry.get(key)(scales[key], legendOptions(defaults[key], options), key => scales[key]);
return legendRegistry.get(key)(scales[key], legendOptions(context, defaults[key], options), key => scales[key]);
};
}

function legendOptions({label, ticks, tickFormat} = {}, options = {}) {
return {label, ticks, tickFormat, ...options};
function legendOptions(context, {label, ticks, tickFormat} = {}, options) {
return inherit(options, context, {label, ticks, tickFormat});
}

function legendColor(color, {
Expand Down Expand Up @@ -71,12 +73,12 @@ function interpolateOpacity(color) {
return t => `rgba(${r},${g},${b},${t})`;
}

export function Legends(scales, options) {
export function Legends(scales, context, options) {
const legends = [];
for (const [key, value] of legendRegistry) {
const o = options[key];
if (o?.legend && (key in scales)) {
const legend = value(scales[key], legendOptions(scales[key], o), key => scales[key]);
const legend = value(scales[key], legendOptions(context, scales[key], o), key => scales[key]);
if (legend != null) legends.push(legend);
}
}
Expand Down
62 changes: 33 additions & 29 deletions src/legends/ramp.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
import {create, quantize, interpolateNumber, piecewise, format, scaleBand, scaleLinear, axisBottom} from "d3";
import {quantize, interpolateNumber, piecewise, format, scaleBand, scaleLinear, axisBottom} from "d3";
import {inferFontVariant} from "../axes.js";
import {Context, create} from "../context.js";
import {map} from "../options.js";
import {interpolatePiecewise} from "../scales/quantitative.js";
import {applyInlineStyles, impliedString, maybeClassName} from "../style.js";

export function legendRamp(color, {
label = color.label,
tickSize = 6,
width = 240,
height = 44 + tickSize,
marginTop = 18,
marginRight = 0,
marginBottom = 16 + tickSize,
marginLeft = 0,
style,
ticks = (width - marginLeft - marginRight) / 64,
tickFormat,
fontVariant = inferFontVariant(color),
round = true,
className
}) {
export function legendRamp(color, options) {
let {
label = color.label,
tickSize = 6,
width = 240,
height = 44 + tickSize,
marginTop = 18,
marginRight = 0,
marginBottom = 16 + tickSize,
marginLeft = 0,
style,
ticks = (width - marginLeft - marginRight) / 64,
tickFormat,
fontVariant = inferFontVariant(color),
round = true,
className
} = options;
const context = Context(options);
className = maybeClassName(className);
if (tickFormat === null) tickFormat = () => null;

const svg = create("svg")
const svg = create("svg", context)
.attr("class", className)
.attr("font-family", "system-ui, sans-serif")
.attr("font-size", 10)
Expand Down Expand Up @@ -83,13 +86,24 @@ export function legendRamp(color, {
)
);

// Construct a 256×1 canvas, filling each pixel using the interpolator.
const n = 256;
const canvas = context.document.createElement("canvas");
canvas.width = n;
canvas.height = 1;
const context2 = canvas.getContext("2d");
for (let i = 0, j = n - 1; i < n; ++i) {
context2.fillStyle = interpolator(i / j);
context2.fillRect(i, 0, 1, 1);
}

svg.append("image")
.attr("x", marginLeft)
.attr("y", marginTop)
.attr("width", width - marginLeft - marginRight)
.attr("height", height - marginTop - marginBottom)
.attr("preserveAspectRatio", "none")
.attr("xlink:href", ramp(interpolator).toDataURL());
.attr("xlink:href", canvas.toDataURL());
}

// Threshold
Expand Down Expand Up @@ -162,13 +176,3 @@ export function legendRamp(color, {

return svg.node();
}

function ramp(color, n = 256) {
const canvas = create("canvas").attr("width", n).attr("height", 1).node();
const context = canvas.getContext("2d");
for (let i = 0; i < n; ++i) {
context.fillStyle = color(i / (n - 1));
context.fillRect(i, 0, 1, 1);
}
return canvas;
}
35 changes: 19 additions & 16 deletions src/legends/swatches.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {create, path} from "d3";
import {path} from "d3";
import {inferFontVariant} from "../axes.js";
import {maybeAutoTickFormat} from "../axis.js";
import {Context, create} from "../context.js";
import {isNoneish, maybeColorChannel, maybeNumberChannel} from "../options.js";
import {applyInlineStyles, impliedString, maybeClassName} from "../style.js";

Expand Down Expand Up @@ -70,23 +71,25 @@ export function legendSymbols(symbol, {
);
}

function legendItems(scale, {
columns,
tickFormat,
fontVariant = inferFontVariant(scale),
// TODO label,
swatchSize = 15,
swatchWidth = swatchSize,
swatchHeight = swatchSize,
marginLeft = 0,
className,
style,
width
} = {}, swatch, swatchStyle) {
function legendItems(scale, options = {}, swatch, swatchStyle) {
let {
columns,
tickFormat,
fontVariant = inferFontVariant(scale),
// TODO label,
swatchSize = 15,
swatchWidth = swatchSize,
swatchHeight = swatchSize,
marginLeft = 0,
className,
style,
width
} = options;
const context = Context(options);
className = maybeClassName(className);
tickFormat = maybeAutoTickFormat(tickFormat, scale.domain);

const swatches = create("div")
const swatches = create("div", context)
.attr("class", className)
.attr("style", `
--swatchWidth: ${+swatchWidth}px;
Expand Down Expand Up @@ -148,7 +151,7 @@ function legendItems(scale, {
.attr("class", `${className}-swatch`)
.call(swatch, scale)
.append(function() {
return document.createTextNode(tickFormat.apply(this, arguments));
return this.ownerDocument.createTextNode(tickFormat.apply(this, arguments));
});
}

Expand Down
9 changes: 5 additions & 4 deletions src/marks/area.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {area as shapeArea, create} from "d3";
import {area as shapeArea} from "d3";
import {create} from "../context.js";
import {Curve} from "../curve.js";
import {Mark} from "../plot.js";
import {first, indexOf, maybeZ, second} from "../options.js";
import {Mark} from "../plot.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, groupIndex} from "../style.js";
import {maybeDenseIntervalX, maybeDenseIntervalY} from "../transforms/bin.js";
import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js";
Expand Down Expand Up @@ -36,9 +37,9 @@ export class Area extends Mark {
filter(index) {
return index;
}
render(index, scales, channels, dimensions) {
render(index, scales, channels, dimensions, context) {
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels;
return create("svg:g")
return create("svg:g", context)
.call(applyIndirectStyles, this, scales, dimensions)
.call(applyTransform, this, scales, 0, 0)
.call(g => g.selectAll()
Expand Down
8 changes: 4 additions & 4 deletions src/marks/arrow.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {create} from "d3";
import {create} from "../context.js";
import {radians} from "../math.js";
import {constant} from "../options.js";
import {Mark} from "../plot.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";
import {maybeSameValue} from "./link.js";
import {constant} from "../options.js";

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

return create("svg:g")
return create("svg:g", context)
.call(applyIndirectStyles, this, scales, dimensions)
.call(applyTransform, this, scales)
.call(g => g.selectAll()
Expand Down
8 changes: 4 additions & 4 deletions src/marks/bar.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {create} from "d3";
import {Mark} from "../plot.js";
import {create} from "../context.js";
import {identity, indexOf, number} from "../options.js";
import {Mark} from "../plot.js";
import {isCollapsed} from "../scales.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js";
import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js";
Expand All @@ -18,9 +18,9 @@ export class AbstractBar extends Mark {
this.rx = impliedString(rx, "auto"); // number or percentage
this.ry = impliedString(ry, "auto");
}
render(index, scales, channels, dimensions) {
render(index, scales, channels, dimensions, context) {
const {rx, ry} = this;
return create("svg:g")
return create("svg:g", context)
.call(applyIndirectStyles, this, scales, dimensions)
.call(this._transform, this, scales)
.call(g => g.selectAll()
Expand Down
Loading