diff --git a/docs/features/legends.md b/docs/features/legends.md
index dd0cfb2762..534334d6fc 100644
--- a/docs/features/legends.md
+++ b/docs/features/legends.md
@@ -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.
diff --git a/src/legends.d.ts b/src/legends.d.ts
index 38dcaf159d..c07d6f3761 100644
--- a/src/legends.d.ts
+++ b/src/legends.d.ts
@@ -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:
*
@@ -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*
@@ -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. */
diff --git a/src/legends.js b/src/legends.js
index 64215ebaf3..77e1fb3f6e 100644
--- a/src/legends.js
+++ b/src/legends.js
@@ -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";
@@ -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;
}
diff --git a/src/legends/swatches.js b/src/legends/swatches.js
index b628a0a5af..9c05717b43 100644
--- a/src/legends/swatches.js
+++ b/src/legends/swatches.js
@@ -29,6 +29,8 @@ export function legendSwatches(color, {opacity, ...options} = {}) {
);
}
+const legendSymbolColor = new WeakSet();
+
export function legendSymbols(
symbol,
{
@@ -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")
@@ -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) {
diff --git a/src/plot.d.ts b/src/plot.d.ts
index 05fc238dc5..2dee66cae4 100644
--- a/src/plot.d.ts
+++ b/src/plot.d.ts
@@ -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";
@@ -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*
@@ -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
@@ -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
diff --git a/src/scales.d.ts b/src/scales.d.ts
index 94112d6180..1817b67fd8 100644
--- a/src/scales.d.ts
+++ b/src/scales.d.ts
@@ -323,6 +323,17 @@ export interface ScaleDefaults extends InsetOptions {
*/
grid?: boolean | string | RangeInterval | Iterable;
+ /**
+ * 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
@@ -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*.
diff --git a/test/output/colorLegendOrdinalRampInline.html b/test/output/colorLegendOrdinalRampInline.html
new file mode 100644
index 0000000000..dfa992ff58
--- /dev/null
+++ b/test/output/colorLegendOrdinalRampInline.html
@@ -0,0 +1,120 @@
+
\ No newline at end of file
diff --git a/test/plots/athletes-sort.ts b/test/plots/athletes-sort.ts
index 1b8b20ad0e..ab35b6f8fa 100644
--- a/test/plots/athletes-sort.ts
+++ b/test/plots/athletes-sort.ts
@@ -16,7 +16,7 @@ export async function athletesSortFacet() {
export async function athletesSortNationality() {
const athletes = await d3.csv("data/athletes.csv", d3.autoType);
return Plot.plot({
- color: {legend: true},
+ legend: true,
marks: [
Plot.dot(
athletes,
@@ -34,7 +34,7 @@ export async function athletesSortNationality() {
export async function athletesSortNullLimit() {
const athletes = await d3.csv("data/athletes.csv", d3.autoType);
return Plot.plot({
- color: {legend: true},
+ legend: true,
marks: [Plot.dot(athletes, {x: "height", y: "weight", stroke: "nationality", sort: {color: null, limit: 10}})]
});
}
@@ -42,7 +42,6 @@ export async function athletesSortNullLimit() {
export async function athletesSortWeightLimit() {
const athletes = await d3.csv("data/athletes.csv", d3.autoType);
return Plot.plot({
- color: {legend: true},
marks: [
Plot.dot(athletes, {x: "weight", y: "nationality", sort: {y: "x", reduce: "median", limit: 10}}),
Plot.tickX(athletes, Plot.groupY({x: "median"}, {x: "weight", y: "nationality", stroke: "red", strokeWidth: 2}))
diff --git a/test/plots/decathlon.ts b/test/plots/decathlon.ts
index 45fc92c1a4..45ba385e40 100644
--- a/test/plots/decathlon.ts
+++ b/test/plots/decathlon.ts
@@ -6,9 +6,7 @@ export async function decathlon() {
return Plot.plot({
grid: true,
inset: 12,
- symbol: {
- legend: true
- },
+ symbol: {legend: true},
marks: [Plot.dot(decathlon, {x: "Long Jump", y: "100 Meters", symbol: "Country", stroke: "Country"})]
});
}
diff --git a/test/plots/legend-color.ts b/test/plots/legend-color.ts
index 031dfdbc78..3609823998 100644
--- a/test/plots/legend-color.ts
+++ b/test/plots/legend-color.ts
@@ -58,6 +58,14 @@ export function colorLegendOrdinalRamp() {
return Plot.legend({color: {type: "ordinal", domain: "ABCDEFGHIJ"}, legend: "ramp"});
}
+export function colorLegendOrdinalRampInline() {
+ return Plot.plot({
+ legend: "ramp",
+ color: {type: "ordinal", domain: "ABCDEFGHIJ"},
+ marks: [Plot.cellX("ABCDEFGHIJ")]
+ });
+}
+
export function colorLegendOrdinalRampTickSize() {
return Plot.legend({
color: {