Skip to content

Commit

Permalink
top-level legend option, and better types
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Nov 23, 2024
1 parent b2b587a commit a71e397
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 105 deletions.
2 changes: 1 addition & 1 deletion docs/features/legends.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Plot does not yet generate legends for the *r* (radius) scale or the *length* sc

## Legend options

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](./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" />

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

Expand Down
162 changes: 86 additions & 76 deletions src/legends.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,89 @@
import type {ScaleName, ScaleOptions} from "./scales.js";

export interface SwatchesLegendOptions {
/**
* The width of the legend in pixels. Defaults to undefined, allowing swatches
* to wrap based on content flow.
*/
width?: number;

/**
* The [CSS columns property][1], for a multi-column layout.
*
* [1]: https://developer.mozilla.org/en-US/docs/Web/CSS/columns
*/
columns?: string;

/** The swatch width and height in pixels; defaults to 15. */
swatchSize?: number;

/** The swatch width in pixels; defaults to **swatchSize**. */
swatchWidth?: number;

/** The swatch height in pixels; defaults to **swatchSize**. */
swatchHeight?: number;
}

export interface RampLegendOptions {
/** The width of the legend in pixels; defaults to 240. */
width?: number;
/** The height of the legend in pixels; defaults to 44 plus **tickSize**. */
height?: number;
/** The top margin in pixels; defaults to 18. */
marginTop?: number;
/** The right margin in pixels; defaults to 0. */
marginRight?: number;
/** The bottom margin in pixels; defaults to 16 plus **tickSize**. */
marginBottom?: number;
/** The left margin in pixels; defaults to 0. */
marginLeft?: number;

/**
* The desired approximate number of axis ticks, or an explicit array of tick
* values, or an interval such as *day* or *month*.
*/
ticks?: ScaleOptions["ticks"];

/**
* The length of axis tick marks in pixels; negative values extend in the
* opposite direction.
*/
tickSize?: ScaleOptions["tickSize"];

/**
* If true, round the output value to the nearest integer (pixel); useful for
* crisp edges when rendering.
*/
round?: ScaleOptions["round"];
}

export interface OpacityLegendOptions extends RampLegendOptions {
/** The constant color the ramp; defaults to black. */
color?: string;
}

export interface ColorLegendOptions extends SwatchesLegendOptions, RampLegendOptions {
/** The desired opacity of the color swatches or ramp; defaults to 1. */
opacity?: number;
}

export interface SymbolLegendOptions extends SwatchesLegendOptions {
/** The desired fill color of symbols; use *color* for a redundant encoding. */
fill?: string;
/** The desired fill opacity of symbols; defaults to 1. */
fillOpacity?: number;
/** The desired stroke color of symbols; use *color* for a redundant encoding. */
stroke?: string;
/** The desired stroke opacity of symbols; defaults to 1. */
strokeOpacity?: number;
/** The desired stroke width of symbols; defaults to 1.5. */
strokeWidth?: number;
/** The desired radius of symbols in pixels; defaults to 4.5. */
r?: number;
}

/** Options for generating a scale legend. */
export interface LegendOptions {
export interface LegendOptions extends ColorLegendOptions, SymbolLegendOptions, OpacityLegendOptions {
/**
* The desired legend type; one of:
*
Expand All @@ -15,6 +97,9 @@ export interface LegendOptions {
*/
legend?: "ramp" | "swatches";

/** A textual label to place above the legend. */
label?: string | null;

/**
* How to format tick values sampled from the scale’s domain. This may be a
* function, which will be passed the tick value *t* and zero-based index *i*
Expand Down Expand Up @@ -44,81 +129,6 @@ export interface LegendOptions {
* default, a random string prefixed with “plot-”.
*/
className?: string | null;

/** The constant color the ramp; defaults to black. For *ramp* *opacity* legends only. */
color?: string;
/** The desired fill color of symbols; use *color* for a redundant encoding. For *symbol* legends only. */
fill?: string;
/** The desired fill opacity of symbols. For *symbol* legends only. */
fillOpacity?: number;
/** The desired opacity of the color swatches or ramp. For *color* legends only. */
opacity?: number;
/** The desired stroke color of symbols; use *color* for a redundant encoding. For *symbol* legends only. */
stroke?: string;
/** The desired stroke opacity of symbols. For *symbol* legends only. */
strokeOpacity?: number;
/** The desired stroke width of symbols. For *symbol* legends only. */
strokeWidth?: number;
/** The desired radius of symbols in pixels. For *symbol* legends only. */
r?: number;

/**
* The width of the legend in pixels. For *ramp* legends, defaults to 240; for
* *swatch* legends, defaults to undefined, allowing the swatches to wrap
* based on content flow.
*/
width?: number;

/**
* The height of the legend in pixels; defaults to 44 plus **tickSize**. For
* *ramp* legends only.
*/
height?: number;

/** The top margin in pixels; defaults to 18. For *ramp* legends only. */
marginTop?: number;
/** The right margin in pixels; defaults to 0. For *ramp* legends only. */
marginRight?: number;
/** The bottom margin in pixels; defaults to 16 plus **tickSize**. For *ramp* legends only. */
marginBottom?: number;
/** The left margin in pixels; defaults to 0. For *ramp* legends only. */
marginLeft?: number;

/** A textual label to place above the legend. For *ramp* legends only. */
label?: string | null;

/**
* The desired approximate number of axis ticks, or an explicit array of tick
* values, or an interval such as *day* or *month*. For *ramp* legends only.
*/
ticks?: ScaleOptions["ticks"];

/**
* The length of axis tick marks in pixels; negative values extend in the
* opposite direction. For *ramp* legends only.
*/
tickSize?: ScaleOptions["tickSize"];

/**
* If true, round the output value to the nearest integer (pixel); useful for
* crisp edges when rendering. For *ramp* legends only.
*/
round?: ScaleOptions["round"];

/**
* The [CSS columns property][1], for a multi-column layout. For *swatches*
* legends only.
*
* [1]: https://developer.mozilla.org/en-US/docs/Web/CSS/columns
*/
columns?: string;

/** The swatch width and height in pixels; defaults to 15; For *swatches* legends only. */
swatchSize?: number;
/** The swatch width in pixels; defaults to **swatchSize**; For *swatches* legends only. */
swatchWidth?: number;
/** The swatch height in pixels; defaults to **swatchSize**; For *swatches* legends only. */
swatchHeight?: number;
}

/** Scale definitions and options for a standalone legend. */
Expand Down
16 changes: 10 additions & 6 deletions src/legends.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {rgb} from "d3";
import {createContext} from "./context.js";
import {legendRamp} from "./legends/ramp.js";
import {legendSwatches, legendSymbols} from "./legends/swatches.js";
import {isSymbolColorLegend, legendSwatches, legendSymbols} from "./legends/swatches.js";
import {inherit, isScaleOptions} from "./options.js";
import {normalizeScale} from "./scales.js";

Expand Down Expand Up @@ -70,12 +70,16 @@ function interpolateOpacity(color) {

export function createLegends(scales, context, options) {
const legends = [];
let hasColor = false;
for (const [key, value] of legendRegistry) {
const o = options[key];
if (o?.legend && key in scales) {
const legend = value(scales[key], legendOptions(context, scales[key], o), (key) => scales[key]);
if (legend != null) legends.push(legend);
}
if (!(key in scales)) continue;
if (key === "color" && hasColor) continue;
const o = inherit(options[key], {legend: options.legend});
if (!o.legend) continue;
const legend = value(scales[key], legendOptions(context, scales[key], o), (key) => scales[key]);
if (legend == null) continue;
if (key === "symbol" && isSymbolColorLegend(legend)) hasColor = true;
legends.push(legend);
}
return legends;
}
15 changes: 14 additions & 1 deletion src/legends/swatches.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export function legendSwatches(color, {opacity, ...options} = {}) {
);
}

const legendSymbolColor = new WeakSet();

export function legendSymbols(
symbol,
{
Expand All @@ -50,7 +52,7 @@ export function legendSymbols(
fillOpacity = maybeNumberChannel(fillOpacity)[1];
strokeOpacity = maybeNumberChannel(strokeOpacity)[1];
strokeWidth = maybeNumberChannel(strokeWidth)[1];
return legendItems(symbol, options, (selection, scale, width, height) =>
const legend = legendItems(symbol, options, (selection, scale, width, height) =>
selection
.append("svg")
.attr("viewBox", "-8 -8 16 16")
Expand All @@ -68,6 +70,17 @@ export function legendSymbols(
return p;
})
);
if (vf === "color" || vs === "color") legendSymbolColor.add(legend);
return legend;
}

/**
* Symbol legends can serve as color legends when the associated symbol channel
* is also bound to the color scale; this test allows Plot to avoid displaying a
* redundant color legend when a satisfying symbol legend is present.
*/
export function isSymbolColorLegend(legend) {
return legendSymbolColor.has(legend);
}

function legendItems(scale, options = {}, swatch) {
Expand Down
8 changes: 4 additions & 4 deletions src/plot.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {ChannelValue} from "./channel.js";
import type {LegendOptions} from "./legends.js";
import type {ColorLegendOptions, LegendOptions, OpacityLegendOptions, SymbolLegendOptions} from "./legends.js";
import type {Data, MarkOptions, Markish} from "./mark.js";
import type {ProjectionFactory, ProjectionImplementation, ProjectionName, ProjectionOptions} from "./projection.js";
import type {Scale, ScaleDefaults, ScaleName, ScaleOptions} from "./scales.js";
Expand Down Expand Up @@ -244,7 +244,7 @@ export interface PlotOptions extends ScaleDefaults {
* scale associated with a channel by specifying the value as a {value, scale}
* object.
*/
color?: ScaleOptions;
color?: ScaleOptions & ColorLegendOptions;

/**
* Options for the *opacity* scale for fill or stroke opacity. The *opacity*
Expand All @@ -257,7 +257,7 @@ export interface PlotOptions extends ScaleDefaults {
* override the scale associated with a channel by specifying the value as a
* {value, scale} object.
*/
opacity?: ScaleOptions;
opacity?: ScaleOptions & OpacityLegendOptions;

/**
* Options for the categorical *symbol* scale for dots. The *symbol* scale
Expand All @@ -270,7 +270,7 @@ export interface PlotOptions extends ScaleDefaults {
* override the scale associated with a channel by specifying the value as a
* {value, scale} object.
*/
symbol?: ScaleOptions;
symbol?: ScaleOptions & SymbolLegendOptions;

/**
* Options for the *length* scale for vectors. The *length* scale defaults to
Expand Down
22 changes: 11 additions & 11 deletions src/scales.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,17 @@ export interface ScaleDefaults extends InsetOptions {
*/
grid?: boolean | string | RangeInterval | Iterable<any>;

/**
* If true, produces a legend for the scale. For quantitative color scales,
* the legend defaults to *ramp* but may be set to *swatches* for discrete
* scale types such as *threshold*. An opacity scale is treated as a color
* scale with varying transparency. The symbol legend is combined with color
* if they encode the same channels.
*
* For *color*, *opacity*, and *symbol* scales only. See also *plot*.legend.
*/
legend?: LegendOptions["legend"] | boolean | null;

/**
* A textual label to show on the axis or legend; if null, show no label. By
* default the scale label is inferred from channel definitions, possibly with
Expand Down Expand Up @@ -534,17 +545,6 @@ export interface ScaleOptions extends ScaleDefaults {
*/
paddingOuter?: number;

/**
* If true, produces a legend for the scale. For quantitative color scales,
* the legend defaults to *ramp* but may be set to *swatches* for discrete
* scale types such as *threshold*. An opacity scale is treated as a color
* scale with varying transparency. The symbol legend is combined with color
* if they encode the same channels.
*
* For *color*, *opacity*, and *symbol* scales only. See also *plot*.legend.
*/
legend?: LegendOptions["legend"] | boolean | null;

/**
* The desired approximate number of axis ticks, or an explicit array of tick
* values, or an interval such as *day* or *month*.
Expand Down
Loading

0 comments on commit a71e397

Please sign in to comment.