Skip to content

legends #539

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

Closed
wants to merge 3 commits into from
Closed
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
66 changes: 65 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,70 @@ Plot.plot({
})
```

All the scale definitions are exposed through the *scales* property of the plot.

```js
color = Plot.plot({…}).scales("color");
color.range // ["red", "blue"]
```

And, to reuse the scale in another plot:

```js
const plot1 = Plot.plot(…);

Plot.plot({
color: plot1.scales("color")
})
```

#### Plot.scale(*scaleOptions*)

```js
Plot.scale(plot1.scales("color"))
```

Returns a [D3 scale](https://github.com/d3/d3-scale) that matches the given Plot scale *options* object.

### Legends

Plot will add a color legend to the figure if the *color*.*legend* option is given.

* *color*.**legend** - a function that is passed the color options, and returns a DOM element to inserted at the top of the figure. If *color.legend* is true, defaults to Plot.legendColor.

#### Plot.legendColor(*scaleOptions*)

Generates a color legend, with swatches for categorical and ordinal scales, and a ramp for continuous scales.

The color swatches can be configured with the following options:
* *color*.**columns** - the number of swatches per row
* *color*.**format** - a format function for the labels
* *color*.**swatchSize** - the size of the swatch (if square)
* *color*.**swatchWidth** - the swatches’ width
* *color*.**swatchHeight** - the swatches’ height
* *color*.**marginLeft** - the legend’s left margin

The continuous color legends can be configured with the following options:
* *color*.**label** - the scale’s label
* *color*.**tickSize** - the tick size
* *color*.**width** - the legend’s width
* *color*.**height** - the legend’s height
* *color*.**marginTop** - the legend’s top margin
* *color*.**marginRight** - the legend’s right margin
* *color*.**marginBottom** - the legend’s bottom margin
* *color*.**marginLeft** - the legend’s left margin
* *color*.**ticks** - number of ticks
* *color*.**tickFormat** - a format function for the legend’s ticks
* *color*.**tickValues** - the legend’s tick values

#### Plot.legendOpacity(*scaleOptions*)

The default opacity legend—rendered as a grayscale color legend. Plot will add an opacity legend to the figure if the *opacity*.*legend* option is given. If *opacity*.*legend* is true, uses the default opacity legend; if *opacity*.*legend* is a function, it is called with the scale’s options and should return a DOM element.

#### Plot.legendRadius(*scaleOptions*)

The default radius legend—rendered as a circles on a common base. Plot will add a radius legend to the figure if the *r*.*legend* option is given. If *r*.*legend* is true, uses the default radius legend; if *r*.*legend* is a function, it is called with the scale’s options and should return a DOM element.

### Position options

The position scales (*x*, *y*, *fx*, and *fy*) support additional options:
Expand Down Expand Up @@ -237,7 +301,7 @@ Plot automatically generates axes for position scales. You can configure these a
* *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)

Plot does not currently generate a legend for the *color*, *radius*, or *opacity* scales, but when it does, we expect that some of the above options will also be used to configure legends. Top-level options are also supported as shorthand: **grid** and **line** (for *x* and *y* only; see also [facet.grid](#facet-options)), **label**, **axis**, **inset**, **round**, **align**, and **padding**.
Top-level options are also supported as shorthand: **grid** (for *x* and *y* only; see [facet.grid](#facet-options)), **label**, **axis**, **inset**, **round**, **align**, and **padding**.

### Color options

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"devDependencies": {
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.0.4",
"canvas": "^2.8.0",
"clean-css": "^5.1.1",
"eslint": "^7.12.1",
"htl": "^0.3.0",
Expand Down
20 changes: 15 additions & 5 deletions src/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function autoAxisTicksK(scale, axis, k) {
}
}

// Mutates axis.{label,labelAnchor,labelOffset}!
// Mutates axis.{label,labelAnchor,labelOffset} and scale.label!
export function autoAxisLabels(channels, scales, {x, y, fx, fy}, dimensions) {
if (fx) {
autoAxisLabelsX(fx, scales.fx, channels.get("fx"));
Expand Down Expand Up @@ -70,24 +70,34 @@ export function autoAxisLabels(channels, scales, {x, y, fx, fy}, dimensions) {

function autoAxisLabelsX(axis, scale, channels) {
if (axis.labelAnchor === undefined) {
axis.labelAnchor = scale.type === "ordinal" ? "center"
axis.labelAnchor = scale.family === "ordinal" ? "center"
: scale.reverse ? "left"
: "right";
}
if (axis.label === undefined) {
axis.label = inferLabel(channels, scale, axis, "x");
}
scale.label = axis.label;
}

function autoAxisLabelsY(axis, opposite, scale, channels) {
if (axis.labelAnchor === undefined) {
axis.labelAnchor = scale.type === "ordinal" ? "center"
axis.labelAnchor = scale.family === "ordinal" ? "center"
: opposite && opposite.axis === "top" ? "bottom" // TODO scale.reverse?
: "top";
}
if (axis.label === undefined) {
axis.label = inferLabel(channels, scale, axis, "y");
}
scale.label = axis.label;
}

export function autoScaleLabel(scale, channels, options) {
if (scale === undefined) return;
if (options !== undefined) scale.label = options.label;
if (scale.label === undefined) {
scale.label = inferLabel(channels, scale, {});
}
}

// Channels can have labels; if all the channels for a given scale are
Expand All @@ -104,8 +114,8 @@ function inferLabel(channels = [], scale, axis, key) {
if (candidate !== undefined) {
const {percent, reverse} = scale;
// Ignore the implicit label for temporal scales if it’s simply “date”.
if (scale.type === "temporal" && /^(date|time|year)$/i.test(candidate)) return;
if (scale.type !== "ordinal" && (key === "x" || key === "y")) {
if (scale.family === "temporal" && /^(date|time|year)$/i.test(candidate)) return;
if (scale.family !== "ordinal" && (key === "x" || key === "y")) {
if (percent) candidate = `${candidate} (%)`;
if (axis.labelAnchor === "center") {
candidate = `${candidate} →`;
Expand Down
24 changes: 24 additions & 0 deletions src/figure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

// Wrap the plot in a figure with a caption, if desired.
export function figureWrap(svg, {width}, caption, legends) {
if (caption == null && legends.length === 0) return svg;
const figure = document.createElement("figure");
figure.style = `max-width: ${width}px`;
if (legends.length > 0) {
const figlegends = document.createElement("div");
figlegends.className = "legends";
figure.appendChild(figlegends);
for (const l of legends) {
if (l instanceof Node) {
figlegends.appendChild(l);
}
}
}
figure.appendChild(svg);
if (caption != null) {
const figcaption = document.createElement("figcaption");
figcaption.appendChild(caption instanceof Node ? caption : document.createTextNode(caption));
figure.appendChild(figcaption);
}
return figure;
}
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export {plot} from "./plot.js";
export {scale} from "./scales.js";
export {Mark, marks, valueof} from "./mark.js";
export {Area, area, areaX, areaY} from "./marks/area.js";
export {BarX, BarY, barX, barY} from "./marks/bar.js";
Expand All @@ -22,3 +23,5 @@ export {windowX, windowY} from "./transforms/window.js";
export {selectFirst, selectLast, selectMaxX, selectMaxY, selectMinX, selectMinY} from "./transforms/select.js";
export {stackX, stackX1, stackX2, stackY, stackY1, stackY2} from "./transforms/stack.js";
export {formatIsoDate, formatWeekday, formatMonth} from "./format.js";
export {legendColor} from "./legends/color.js";
export {legendOpacity} from "./legends/opacity.js";
21 changes: 21 additions & 0 deletions src/legends.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {registry} from "./scales/index.js";
import {legendColor} from "./legends/color.js";
import {legendOpacity} from "./legends/opacity.js";
import {legendRadius} from "./legends/radius.js";

export function createLegends(descriptors, dimensions) {
const legends = [];
for (const [key] of registry) {
const scale = descriptors(key);
if (scale === undefined) continue;
let {legend, ...options} = scale;
if (key === "color" && legend === true) legend = legendColor;
if (key === "opacity" && legend === true) legend = legendOpacity;
if (key === "r" && legend === true) legend = legendRadius;
if (typeof legend === "function") {
const l = legend(options, dimensions);
if (l instanceof Node) legends.push(l);
}
}
return legends;
}
19 changes: 19 additions & 0 deletions src/legends/color.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {scale} from "../scales.js";
import {legendRamp} from "./ramp.js";
import {legendSwatches} from "./swatches.js";

export function legendColor(color, {width: maxWidth = 640} = {}) {
if (typeof color === "object" && "scales" in color && typeof color.scales === "function") {
color = color.scales("color");
}
if (!color) return;
const {...options} = color;
switch (options.type) {
case "ordinal": case "categorical":
return legendSwatches(scale(options), options);
default:
options.key = "color"; // for diverging
if (options.width === undefined) options.width = Math.min(240, maxWidth);
return legendRamp(scale(options), options);
}
}
13 changes: 13 additions & 0 deletions src/legends/opacity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {legendColor} from "./color.js";

export function legendOpacity(plotOrScale, dimensions) {
if (!plotOrScale) return;
const opacity = "scales" in plotOrScale ? plotOrScale.scales.opacity : plotOrScale;
if (!opacity) return;
return legendColor({
...opacity,
range: undefined,
interpolate: undefined,
scheme: "greys"
}, dimensions);
}
64 changes: 64 additions & 0 deletions src/legends/radius.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {plot} from "../plot.js";
import {link} from "../marks/link.js";
import {text} from "../marks/text.js";
import {dot} from "../marks/dot.js";
import {scale} from "../scales.js";

export function legendRadius({
label,
ticks = 5,
tickFormat = (d) => d,
strokeWidth = 0.5,
strokeDasharray = [5, 4],
minStep = 8,
gap = 20,
...r
}) {
const s = scale(r);
const r0 = s.range()[1];

const shiftY = label ? 10 : 0;

let h = Infinity;
const values = s
.ticks(ticks)
.reverse()
.filter((t) => h - s(t) > minStep / 2 && (h = s(t)));

return plot({
x: { type: "identity", axis: null },
r: { type: "identity" },
y: { type: "identity", axis: null },
marks: [
link(values, {
x1: r0 + 2,
y1: (d) => 8 + 2 * r0 - 2 * s(d) + shiftY,
x2: 2 * r0 + 2 + gap,
y2: (d) => 8 + 2 * r0 - 2 * s(d) + shiftY,
strokeWidth: strokeWidth / 2,
strokeDasharray
}),
dot(values, {
r: s,
x: r0 + 2,
y: (d) => 8 + 2 * r0 - s(d) + shiftY,
strokeWidth
}),
text(values, {
x: 2 * r0 + 2 + gap,
y: (d) => 8 + 2 * r0 - 2 * s(d) + shiftY,
textAnchor: "start",
dx: 4,
text: tickFormat
}),
text(label ? [label] : [], {
x: 0,
y: 6,
textAnchor: "start",
fontWeight: "bold",
text: tickFormat
})
],
height: 2 * r0 + 10 + shiftY
});
}
Loading