Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
64 changes: 58 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ Each scale’s options are specified as a nested options object with the corresp
* **r** - radius (size)
* **color** - fill or stroke
* **opacity** - fill or stroke opacity
* **length** - linear length (for [vectors](#vector))
* **symbol** - categorical symbol (for [dots](#dot))

For example, to set the domain for the *x* and *y* scales:

Expand Down Expand Up @@ -496,11 +498,11 @@ When the *include* or *exclude* facet mode is chosen, the mark data must be para

## Legends

Plot can generate legends for *color* and *opacity* [scales](#scale-options). (An opacity scale is treated as a color scale with varying transparency.) For an inline legend, use the *scale*.**legend** option:
Plot can generate legends for *color*, *opacity*, and *symbol* [scales](#scale-options). (An opacity scale is treated as a color scale with varying transparency.) For an inline legend, use the *scale*.**legend** option:

* *scale*.**legend** - if truthy, generate a legend for the given scale

If the *scale*.**legend** option is true, the default legend will be produced for the scale; otherwise, the meaning of the *legend* option depends on the scale. For quantitative color scales, it defaults to *ramp* but may be set to *swatches* for a discrete scale (most commonly for *threshold* color scales); for ordinal color scales, only the *swatches* value is supported.
If the *scale*.**legend** option is true, the default legend will be produced for the scale; otherwise, the meaning of the *legend* option depends on the scale. For quantitative color scales, it defaults to *ramp* but may be set to *swatches* for a discrete scale (most commonly for *threshold* color scales); for ordinal color scales and symbol scales, only the *swatches* value is supported.

### *plot*.legend(*name*, *options*)

Expand All @@ -524,6 +526,17 @@ Categorical and ordinal color legends are rendered as swatches, unless *options*
* *options*.**className** - a class name, that defaults to a randomly generated string scoping the styles
* *options*.**width** - the legend’s width (in pixels)

Symbol legends are rendered as swatches and share the same options as above. In addition, symbol legends support the following additional options:

* *options*.**fill** - the symbol fill color
* *options*.**fillOpacity** - the symbol fill opacity; defaults to 1
* *options*.**stroke** - the symbol stroke color
* *options*.**strokeOpacity** - the symbol stroke opacity; defaults to 1
* *options*.**strokeWidth** - the symbol stroke width; defaults to 1.5
* *options*.**r** - the symbol radius; defaults to 4.5 pixels

The **fill** and **stroke** options can be specified as “color” to use the corresponding color encoding, for when the symbol encoding is redundant. The **fill** defaults to none. The **stroke** defaults to currentColor if the fill is none, and to none otherwise. The **fill** and **stroke** options may also be inherited from the corresponding options on an associated dot mark.

Continuous color legends are rendered as a ramp, and can be configured with the following options:

* *options*.**label** - the scale’s label
Expand Down Expand Up @@ -783,17 +796,21 @@ Equivalent to [Plot.cell](#plotcelldata-options), except that if the **y** optio

[<img src="./img/dot.png" width="320" height="198" alt="a scatterplot">](https://observablehq.com/@observablehq/plot-dot)

[Source](./src/marks/dot.js) · [Examples](https://observablehq.com/@observablehq/plot-dot) · Draws circles (and in the future, possibly other symbols) as in a scatterplot.
[Source](./src/marks/dot.js) · [Examples](https://observablehq.com/@observablehq/plot-dot) · Draws circles, or other symbols, as in a scatterplot.

In addition to the [standard mark options](#marks), the following optional channels are supported:

* **x** - the horizontal position; bound to the *x* scale
* **y** - the vertical position; bound to the *y* scale
* **r** - the radius (area); bound to the *radius* scale, which defaults to *sqrt*
* **rotate** - the rotation angle in degrees clockwise; defaults to 0
* **symbol** - the categorical symbol; bound to the *symbol* scale; defaults to circle

If the **x** channel is not specified, dots will be horizontally centered in the plot (or facet). Likewise if the **y** channel is not specified, dots will vertically centered in the plot (or facet). Typically either *x*, *y*, or both are specified.

The **r** option defaults to three pixels and can be specified as either a channel or constant. When the radius is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. Dots with a nonpositive radius are not drawn. The **stroke** defaults to none. The **fill** defaults to currentColor if the stroke is none, and to none otherwise. The **strokeWidth** defaults to 1.5.
The **r** option can be specified as either a channel or constant. When the radius is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. The radius defaults to 4.5 pixels when using the **symbol** channel, and otherwise 3 pixels. Dots with a nonpositive radius are not drawn.

The **stroke** defaults to none. The **fill** defaults to currentColor if the stroke is none, and to none otherwise. The **strokeWidth** defaults to 1.5. The **rotate** and **symbol** options can be specified as either channels or constants. When rotate is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. When symbol is a valid symbol name or symbol object (implementing the draw method), it is interpreted as a constant; otherwise it is interpreted as a channel.

Dots are drawn in input order, with the last data drawn on top. If sorting is needed, say to mitigate overplotting by drawing the smallest dots on top, consider a [sort and reverse transform](#transforms).

Expand Down Expand Up @@ -1028,7 +1045,7 @@ In addition to the [standard mark options](#marks), the following optional chann
* **x** - the horizontal position; bound to the *x* scale
* **y** - the vertical position; bound to the *y* scale
* **fontSize** - the font size in pixels
* **rotate** - the rotation in degrees clockwise
* **rotate** - the rotation angle in degrees clockwise

The following text-specific constant options are also supported:

Expand All @@ -1037,7 +1054,7 @@ The following text-specific constant options are also supported:
* **fontStyle** - the [font style](https://developer.mozilla.org/en-US/docs/Web/CSS/font-style); defaults to normal
* **fontVariant** - the [font variant](https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant); defaults to normal
* **fontWeight** - the [font weight](https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight); defaults to normal
* **rotate** - the rotation in degrees clockwise; defaults to 0
* **rotate** - the rotation angle in degrees clockwise; defaults to 0

For text marks, the **dx** and **dy** options can be specified either as numbers representing pixels or as a string including units. For example, `"1em"` shifts the text by one [em](https://en.wikipedia.org/wiki/Em_(typography)), which is proportional to the **fontSize**. The **fontSize** and **rotate** options can be specified as either channels or constants. When fontSize or rotate is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel.

Expand Down Expand Up @@ -1093,6 +1110,41 @@ The following optional channels are supported:

If the **x** channel is not specified, the tick will span the full vertical extent of the plot (or facet).

### Vector

[<img src="./img/vector.png" width="320" height="200" alt="a vector field">](https://observablehq.com/@observablehq/plot-vector)

[Source](./src/marks/vector.js) · [Examples](https://observablehq.com/@observablehq/plot-vector) · Draws little arrows as in a vector field.

In addition to the [standard mark options](#marks), the following optional channels are supported:

* **x** - the horizontal position; bound to the *x* scale
* **y** - the vertical position; bound to the *y* scale
* **length** - the length in pixels; bound to the *length* scale; defaults to 12
* **rotate** - the rotation angle in degrees clockwise; defaults to 0

The following options are also supported:

* **anchor** - one of *start*, *middle*, or *end*; defaults to *middle*

If the **anchor** is *start*, the arrow will start at the given *xy* position and point in the direction given by the rotation angle. If the **anchor** is *end*, the arrow will maintain the same orientation, but be positioned such that it ends in the given *xy* position. If the **anchor** is *middle*, the arrow will be likewise be positioned such that its midpoint intersects the given *xy* position.

If the **x** channel is not specified, vectors will be horizontally centered in the plot (or facet). Likewise if the **y** channel is not specified, vectors will vertically centered in the plot (or facet). Typically either *x*, *y*, or both are specified.

The **rotate** and **length** options can be specified as either channels or constants. When specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. The length defaults to 12 pixels, and the rotate defaults to 0 degrees (pointing up↑). Vectors with a negative length will be drawn inverted. Positive angles proceed clockwise from noon.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The **rotate** and **length** options can be specified as either channels or constants. When specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. The length defaults to 12 pixels, and the rotate defaults to 0 degrees (pointing up↑). Vectors with a negative length will be drawn inverted. Positive angles proceed clockwise from noon.
The **rotate** and **length** options can be specified as either channels or constants. When specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. The length defaults to 12 pixels, and the rotate defaults to 0 degrees (pointing up↑). Vectors with a negative length will be drawn inverted. Vectors of length 0 will show as a single pixel; use NaN to remove. Positive angles proceed clockwise from noon.

(not sure about this wording)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don’t think saying a single pixel is correct… since it depends on the strokeWidth? Or possibly you see nothing? In any case, probably better to elaborate in the Marks section above, near here:

Plot.dot will not generate circles with null, undefined or negative radius, or null or undefined coordinates.


The **stroke** defaults to currentColor. The **strokeWidth** defaults to 1.5, and the **strokeLinecap** defaults to *round*.

Vectors are drawn in input order, with the last data drawn on top. If sorting is needed, say to mitigate overplotting by drawing the smallest vectors on top, consider a [sort and reverse transform](#transforms).

#### Plot.vector(*data*, *options*)

```js
Plot.vector(wind, {x: "longitude", y: "latitude", length: "speed", rotate: "direction"})
```

Returns a new vector with the given *data* and *options*. If neither the **x** nor **y** options are specified, *data* is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].

### Plot.marks(...*marks*)

A convenience method for composing a mark from a series of other marks. Returns an array of marks that implements the *mark*.plot function.
Expand Down
Binary file added img/vector.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"snowpack": "3"
},
"dependencies": {
"d3": "7",
"d3": "^7.3.0",
"isoformat": "0.2"
},
"engines": {
Expand Down
3 changes: 1 addition & 2 deletions src/axis.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {axisTop, axisBottom, axisRight, axisLeft, create, format, utcFormat} from "d3";
import {formatIsoDate} from "./format.js";
import {boolean, take, number, string, keyword, maybeKeyword, constant, isTemporal} from "./mark.js";
import {radians} from "./math.js";
import {impliedString} from "./style.js";

export class AxisX {
Expand Down Expand Up @@ -234,8 +235,6 @@ function createAxis(axis, scale, {ticks, tickSize, tickPadding, tickFormat}) {
.tickValues(Array.isArray(ticks) ? ticks : null);
}

const radians = Math.PI / 180;

function maybeTickRotate(g, rotate) {
if (!(rotate = +rotate)) return;
for (const text of g.selectAll("text")) {
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export {Rect, rect, rectX, rectY} from "./marks/rect.js";
export {RuleX, RuleY, ruleX, ruleY} from "./marks/rule.js";
export {Text, text, textX, textY} from "./marks/text.js";
export {TickX, TickY, tickX, tickY} from "./marks/tick.js";
export {Vector, vector} from "./marks/vector.js";
export {filter} from "./transforms/filter.js";
export {reverse} from "./transforms/reverse.js";
export {sort, shuffle} from "./transforms/sort.js";
Expand Down
40 changes: 35 additions & 5 deletions src/legends.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import {rgb} from "d3";
import {normalizeScale} from "./scales.js";
import {legendColor} from "./legends/color.js";
import {legendOpacity} from "./legends/opacity.js";
import {legendRamp} from "./legends/ramp.js";
import {legendSwatches, legendSymbols} from "./legends/swatches.js";
import {isObject} from "./mark.js";

const legendRegistry = new Map([
["color", legendColor],
["symbol", legendSymbols],
["opacity", legendOpacity]
]);

export function legend(options = {}) {
for (const [key, value] of legendRegistry) {
const scale = options[key];
if (isObject(scale)) { // e.g., ignore {color: "red"}
return value(normalizeScale(key, scale), legendOptions(scale, options));
return value(normalizeScale(key, scale), legendOptions(scale, options), key => isObject(options[key]) ? normalizeScale(key, options[key]) : null);
}
}
throw new Error("unknown legend type");
Expand All @@ -22,20 +24,48 @@ export function exposeLegends(scales, 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));
return legendRegistry.get(key)(scales[key], legendOptions(defaults[key], options), key => scales[key]);
};
}

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

function legendColor(color, {
legend = true,
...options
}) {
if (legend === true) legend = color.type === "ordinal" ? "swatches" : "ramp";
switch (`${legend}`.toLowerCase()) {
case "swatches": return legendSwatches(color, options);
case "ramp": return legendRamp(color, options);
default: throw new Error(`unknown legend type: ${legend}`);
}
}

function legendOpacity({type, interpolate, ...scale}, {
legend = true,
color = rgb(0, 0, 0),
...options
}) {
if (!interpolate) throw new Error(`${type} opacity scales are not supported`);
if (legend === true) legend = "ramp";
if (`${legend}`.toLowerCase() !== "ramp") throw new Error(`${legend} opacity legends are not supported`);
return legendColor({type, ...scale, interpolate: interpolateOpacity(color)}, {legend, ...options});
}

function interpolateOpacity(color) {
const {r, g, b} = rgb(color) || rgb(0, 0, 0); // treat invalid color as black
return t => `rgba(${r},${g},${b},${t})`;
}

export function Legends(scales, options) {
const legends = [];
for (const [key, value] of legendRegistry) {
const o = options[key];
if (o && o.legend) {
legends.push(value(scales[key], legendOptions(scales[key], o)));
legends.push(value(scales[key], legendOptions(scales[key], o), key => scales[key]));
}
}
return legends;
Expand Down
14 changes: 0 additions & 14 deletions src/legends/color.js

This file was deleted.

20 changes: 0 additions & 20 deletions src/legends/opacity.js

This file was deleted.

Loading