Skip to content
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

top-level legend option, and better types #2249

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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