Skip to content

Commit 1b7e76a

Browse files
authored
top-level legend option, and better types (#2249)
1 parent 7c48dac commit 1b7e76a

File tree

10 files changed

+257
-105
lines changed

10 files changed

+257
-105
lines changed

docs/features/legends.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ Plot does not yet generate legends for the *r* (radius) scale or the *length* sc
7676

7777
## Legend options
7878

79-
If the **legend** [scale option](./scales.md#scale-options) 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.
79+
If the **legend** [scale option](./scales.md#scale-options) 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. If the **legend* scale option is undefined, it will be inherited from the top-level **legend** plot option. <VersionBadge pr="2247" />
8080

8181
<!-- TODO Describe the color and opacity options. -->
8282

src/legends.d.ts

+86-76
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,89 @@
11
import type {ScaleName, ScaleOptions} from "./scales.js";
22

3+
export interface SwatchesLegendOptions {
4+
/**
5+
* The width of the legend in pixels. Defaults to undefined, allowing swatches
6+
* to wrap based on content flow.
7+
*/
8+
width?: number;
9+
10+
/**
11+
* The [CSS columns property][1], for a multi-column layout.
12+
*
13+
* [1]: https://developer.mozilla.org/en-US/docs/Web/CSS/columns
14+
*/
15+
columns?: string;
16+
17+
/** The swatch width and height in pixels; defaults to 15. */
18+
swatchSize?: number;
19+
20+
/** The swatch width in pixels; defaults to **swatchSize**. */
21+
swatchWidth?: number;
22+
23+
/** The swatch height in pixels; defaults to **swatchSize**. */
24+
swatchHeight?: number;
25+
}
26+
27+
export interface RampLegendOptions {
28+
/** The width of the legend in pixels; defaults to 240. */
29+
width?: number;
30+
/** The height of the legend in pixels; defaults to 44 plus **tickSize**. */
31+
height?: number;
32+
/** The top margin in pixels; defaults to 18. */
33+
marginTop?: number;
34+
/** The right margin in pixels; defaults to 0. */
35+
marginRight?: number;
36+
/** The bottom margin in pixels; defaults to 16 plus **tickSize**. */
37+
marginBottom?: number;
38+
/** The left margin in pixels; defaults to 0. */
39+
marginLeft?: number;
40+
41+
/**
42+
* The desired approximate number of axis ticks, or an explicit array of tick
43+
* values, or an interval such as *day* or *month*.
44+
*/
45+
ticks?: ScaleOptions["ticks"];
46+
47+
/**
48+
* The length of axis tick marks in pixels; negative values extend in the
49+
* opposite direction.
50+
*/
51+
tickSize?: ScaleOptions["tickSize"];
52+
53+
/**
54+
* If true, round the output value to the nearest integer (pixel); useful for
55+
* crisp edges when rendering.
56+
*/
57+
round?: ScaleOptions["round"];
58+
}
59+
60+
export interface OpacityLegendOptions extends RampLegendOptions {
61+
/** The constant color the ramp; defaults to black. */
62+
color?: string;
63+
}
64+
65+
export interface ColorLegendOptions extends SwatchesLegendOptions, RampLegendOptions {
66+
/** The desired opacity of the color swatches or ramp; defaults to 1. */
67+
opacity?: number;
68+
}
69+
70+
export interface SymbolLegendOptions extends SwatchesLegendOptions {
71+
/** The desired fill color of symbols; use *color* for a redundant encoding. */
72+
fill?: string;
73+
/** The desired fill opacity of symbols; defaults to 1. */
74+
fillOpacity?: number;
75+
/** The desired stroke color of symbols; use *color* for a redundant encoding. */
76+
stroke?: string;
77+
/** The desired stroke opacity of symbols; defaults to 1. */
78+
strokeOpacity?: number;
79+
/** The desired stroke width of symbols; defaults to 1.5. */
80+
strokeWidth?: number;
81+
/** The desired radius of symbols in pixels; defaults to 4.5. */
82+
r?: number;
83+
}
84+
385
/** Options for generating a scale legend. */
4-
export interface LegendOptions {
86+
export interface LegendOptions extends ColorLegendOptions, SymbolLegendOptions, OpacityLegendOptions {
587
/**
688
* The desired legend type; one of:
789
*
@@ -15,6 +97,9 @@ export interface LegendOptions {
1597
*/
1698
legend?: "ramp" | "swatches";
1799

100+
/** A textual label to place above the legend. */
101+
label?: string | null;
102+
18103
/**
19104
* How to format tick values sampled from the scale’s domain. This may be a
20105
* function, which will be passed the tick value *t* and zero-based index *i*
@@ -44,81 +129,6 @@ export interface LegendOptions {
44129
* default, a random string prefixed with “plot-”.
45130
*/
46131
className?: string | null;
47-
48-
/** The constant color the ramp; defaults to black. For *ramp* *opacity* legends only. */
49-
color?: string;
50-
/** The desired fill color of symbols; use *color* for a redundant encoding. For *symbol* legends only. */
51-
fill?: string;
52-
/** The desired fill opacity of symbols. For *symbol* legends only. */
53-
fillOpacity?: number;
54-
/** The desired opacity of the color swatches or ramp. For *color* legends only. */
55-
opacity?: number;
56-
/** The desired stroke color of symbols; use *color* for a redundant encoding. For *symbol* legends only. */
57-
stroke?: string;
58-
/** The desired stroke opacity of symbols. For *symbol* legends only. */
59-
strokeOpacity?: number;
60-
/** The desired stroke width of symbols. For *symbol* legends only. */
61-
strokeWidth?: number;
62-
/** The desired radius of symbols in pixels. For *symbol* legends only. */
63-
r?: number;
64-
65-
/**
66-
* The width of the legend in pixels. For *ramp* legends, defaults to 240; for
67-
* *swatch* legends, defaults to undefined, allowing the swatches to wrap
68-
* based on content flow.
69-
*/
70-
width?: number;
71-
72-
/**
73-
* The height of the legend in pixels; defaults to 44 plus **tickSize**. For
74-
* *ramp* legends only.
75-
*/
76-
height?: number;
77-
78-
/** The top margin in pixels; defaults to 18. For *ramp* legends only. */
79-
marginTop?: number;
80-
/** The right margin in pixels; defaults to 0. For *ramp* legends only. */
81-
marginRight?: number;
82-
/** The bottom margin in pixels; defaults to 16 plus **tickSize**. For *ramp* legends only. */
83-
marginBottom?: number;
84-
/** The left margin in pixels; defaults to 0. For *ramp* legends only. */
85-
marginLeft?: number;
86-
87-
/** A textual label to place above the legend. For *ramp* legends only. */
88-
label?: string | null;
89-
90-
/**
91-
* The desired approximate number of axis ticks, or an explicit array of tick
92-
* values, or an interval such as *day* or *month*. For *ramp* legends only.
93-
*/
94-
ticks?: ScaleOptions["ticks"];
95-
96-
/**
97-
* The length of axis tick marks in pixels; negative values extend in the
98-
* opposite direction. For *ramp* legends only.
99-
*/
100-
tickSize?: ScaleOptions["tickSize"];
101-
102-
/**
103-
* If true, round the output value to the nearest integer (pixel); useful for
104-
* crisp edges when rendering. For *ramp* legends only.
105-
*/
106-
round?: ScaleOptions["round"];
107-
108-
/**
109-
* The [CSS columns property][1], for a multi-column layout. For *swatches*
110-
* legends only.
111-
*
112-
* [1]: https://developer.mozilla.org/en-US/docs/Web/CSS/columns
113-
*/
114-
columns?: string;
115-
116-
/** The swatch width and height in pixels; defaults to 15; For *swatches* legends only. */
117-
swatchSize?: number;
118-
/** The swatch width in pixels; defaults to **swatchSize**; For *swatches* legends only. */
119-
swatchWidth?: number;
120-
/** The swatch height in pixels; defaults to **swatchSize**; For *swatches* legends only. */
121-
swatchHeight?: number;
122132
}
123133

124134
/** Scale definitions and options for a standalone legend. */

src/legends.js

+10-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {rgb} from "d3";
22
import {createContext} from "./context.js";
33
import {legendRamp} from "./legends/ramp.js";
4-
import {legendSwatches, legendSymbols} from "./legends/swatches.js";
4+
import {isSymbolColorLegend, legendSwatches, legendSymbols} from "./legends/swatches.js";
55
import {inherit, isScaleOptions} from "./options.js";
66
import {normalizeScale} from "./scales.js";
77

@@ -70,12 +70,16 @@ function interpolateOpacity(color) {
7070

7171
export function createLegends(scales, context, options) {
7272
const legends = [];
73+
let hasColor = false;
7374
for (const [key, value] of legendRegistry) {
74-
const o = options[key];
75-
if (o?.legend && key in scales) {
76-
const legend = value(scales[key], legendOptions(context, scales[key], o), (key) => scales[key]);
77-
if (legend != null) legends.push(legend);
78-
}
75+
if (!(key in scales)) continue;
76+
if (key === "color" && hasColor) continue;
77+
const o = inherit(options[key], {legend: options.legend});
78+
if (!o.legend) continue;
79+
const legend = value(scales[key], legendOptions(context, scales[key], o), (key) => scales[key]);
80+
if (legend == null) continue;
81+
if (key === "symbol" && isSymbolColorLegend(legend)) hasColor = true;
82+
legends.push(legend);
7983
}
8084
return legends;
8185
}

src/legends/swatches.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export function legendSwatches(color, {opacity, ...options} = {}) {
2929
);
3030
}
3131

32+
const legendSymbolColor = new WeakSet();
33+
3234
export function legendSymbols(
3335
symbol,
3436
{
@@ -50,7 +52,7 @@ export function legendSymbols(
5052
fillOpacity = maybeNumberChannel(fillOpacity)[1];
5153
strokeOpacity = maybeNumberChannel(strokeOpacity)[1];
5254
strokeWidth = maybeNumberChannel(strokeWidth)[1];
53-
return legendItems(symbol, options, (selection, scale, width, height) =>
55+
const legend = legendItems(symbol, options, (selection, scale, width, height) =>
5456
selection
5557
.append("svg")
5658
.attr("viewBox", "-8 -8 16 16")
@@ -68,6 +70,17 @@ export function legendSymbols(
6870
return p;
6971
})
7072
);
73+
if (vf === "color" || vs === "color") legendSymbolColor.add(legend);
74+
return legend;
75+
}
76+
77+
/**
78+
* Symbol legends can serve as color legends when the associated symbol channel
79+
* is also bound to the color scale; this test allows Plot to avoid displaying a
80+
* redundant color legend when a satisfying symbol legend is present.
81+
*/
82+
export function isSymbolColorLegend(legend) {
83+
return legendSymbolColor.has(legend);
7184
}
7285

7386
function legendItems(scale, options = {}, swatch) {

src/plot.d.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {ChannelValue} from "./channel.js";
2-
import type {LegendOptions} from "./legends.js";
2+
import type {ColorLegendOptions, LegendOptions, OpacityLegendOptions, SymbolLegendOptions} from "./legends.js";
33
import type {Data, MarkOptions, Markish} from "./mark.js";
44
import type {ProjectionFactory, ProjectionImplementation, ProjectionName, ProjectionOptions} from "./projection.js";
55
import type {Scale, ScaleDefaults, ScaleName, ScaleOptions} from "./scales.js";
@@ -246,7 +246,7 @@ export interface PlotOptions extends ScaleDefaults {
246246
* scale associated with a channel by specifying the value as a {value, scale}
247247
* object.
248248
*/
249-
color?: ScaleOptions;
249+
color?: ScaleOptions & ColorLegendOptions;
250250

251251
/**
252252
* Options for the *opacity* scale for fill or stroke opacity. The *opacity*
@@ -259,7 +259,7 @@ export interface PlotOptions extends ScaleDefaults {
259259
* override the scale associated with a channel by specifying the value as a
260260
* {value, scale} object.
261261
*/
262-
opacity?: ScaleOptions;
262+
opacity?: ScaleOptions & OpacityLegendOptions;
263263

264264
/**
265265
* Options for the categorical *symbol* scale for dots. The *symbol* scale
@@ -272,7 +272,7 @@ export interface PlotOptions extends ScaleDefaults {
272272
* override the scale associated with a channel by specifying the value as a
273273
* {value, scale} object.
274274
*/
275-
symbol?: ScaleOptions;
275+
symbol?: ScaleOptions & SymbolLegendOptions;
276276

277277
/**
278278
* Options for the *length* scale for vectors. The *length* scale defaults to

src/scales.d.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,17 @@ export interface ScaleDefaults extends InsetOptions {
323323
*/
324324
grid?: boolean | string | RangeInterval | Iterable<any>;
325325

326+
/**
327+
* If true, produces a legend for the scale. For quantitative color scales,
328+
* the legend defaults to *ramp* but may be set to *swatches* for discrete
329+
* scale types such as *threshold*. An opacity scale is treated as a color
330+
* scale with varying transparency. The symbol legend is combined with color
331+
* if they encode the same channels.
332+
*
333+
* For *color*, *opacity*, and *symbol* scales only. See also *plot*.legend.
334+
*/
335+
legend?: LegendOptions["legend"] | boolean | null;
336+
326337
/**
327338
* A textual label to show on the axis or legend; if null, show no label. By
328339
* default the scale label is inferred from channel definitions, possibly with
@@ -534,17 +545,6 @@ export interface ScaleOptions extends ScaleDefaults {
534545
*/
535546
paddingOuter?: number;
536547

537-
/**
538-
* If true, produces a legend for the scale. For quantitative color scales,
539-
* the legend defaults to *ramp* but may be set to *swatches* for discrete
540-
* scale types such as *threshold*. An opacity scale is treated as a color
541-
* scale with varying transparency. The symbol legend is combined with color
542-
* if they encode the same channels.
543-
*
544-
* For *color*, *opacity*, and *symbol* scales only. See also *plot*.legend.
545-
*/
546-
legend?: LegendOptions["legend"] | boolean | null;
547-
548548
/**
549549
* The desired approximate number of axis ticks, or an explicit array of tick
550550
* values, or an interval such as *day* or *month*.

0 commit comments

Comments
 (0)