diff --git a/src/legends/ramp.js b/src/legends/ramp.js
index 3dcaf411c6..81b0b0a2d6 100644
--- a/src/legends/ramp.js
+++ b/src/legends/ramp.js
@@ -1,4 +1,4 @@
-import {quantize, interpolateNumber, piecewise, format, scaleBand, scaleLinear, axisBottom} from "d3";
+import {quantize, interpolateNumber, piecewise, format, scaleBand, scaleLinear, axisBottom, scaleThreshold} from "d3";
import {inferFontVariant} from "../axes.js";
import {createContext, create} from "../context.js";
import {map, maybeNumberChannel} from "../options.js";
@@ -58,7 +58,7 @@ export function legendRamp(color, options) {
// Some D3 scales use scale.interpolate, some scale.interpolator, and some
// scale.round; this normalizes the API so it works with all scale types.
- const applyRange = round ? (x, range) => x.rangeRound(range) : (x, range) => x.range(range);
+ const applyRange = round ? (x, range) => (x.rangeRound ?? x.range)(range) : (x, range) => x.range(range);
const {type, domain, range, interpolate, scale, pivot} = color;
@@ -70,18 +70,27 @@ export function legendRamp(color, options) {
const interpolator =
range === undefined
? interpolate
+ : type === "threshold"
+ ? scaleThreshold(
+ Array.from(domain, (_, i) => (i + 1) / (domain.length + 1)),
+ Array.from(range, interpolate)
+ )
: piecewise(interpolate.length === 1 ? interpolatePiecewise(interpolate) : interpolate, range);
// Construct a D3 scale of the same type, but with a range that evenly
// divides the horizontal extent of the legend. (In the common case, the
// domain.length is two, and so the range is simply the extent.) For a
// diverging scale, we need an extra point in the range for the pivot such
- // that the pivot is always drawn in the middle.
+ // that the pivot is always drawn in the middle. For a threshold scale, we
+ // add some space below and above.
x = applyRange(
scale.copy(),
quantize(
interpolateNumber(marginLeft, width - marginRight),
- Math.min(domain.length + (pivot !== undefined), range === undefined ? Infinity : range.length)
+ Math.min(
+ domain.length + (pivot !== undefined) + 2 * (type === "threshold"),
+ range === undefined ? Infinity : range.length + +(type === "threshold")
+ )
)
);
diff --git a/test/output/ordinalOpacityRamp.html b/test/output/ordinalOpacityRamp.html
new file mode 100644
index 0000000000..d8b388244d
--- /dev/null
+++ b/test/output/ordinalOpacityRamp.html
@@ -0,0 +1,109 @@
+
\ No newline at end of file
diff --git a/test/output/ordinalOpacityThreshold.html b/test/output/ordinalOpacityThreshold.html
new file mode 100644
index 0000000000..36b2f21727
--- /dev/null
+++ b/test/output/ordinalOpacityThreshold.html
@@ -0,0 +1,81 @@
+
\ No newline at end of file
diff --git a/test/plots/ordinal-opacity.ts b/test/plots/ordinal-opacity.ts
index 65438dfb5c..b0396ece1d 100644
--- a/test/plots/ordinal-opacity.ts
+++ b/test/plots/ordinal-opacity.ts
@@ -8,3 +8,15 @@ export async function ordinalOpacity() {
export async function ordinalOpacityImplicitZero() {
return Plot.cellX(d3.range(2, 10), {fill: "red", opacity: Plot.identity}).plot({opacity: {type: "ordinal"}});
}
+
+export async function ordinalOpacityRamp() {
+ return Plot.cellX(d3.range(10), {fill: "red", opacity: Plot.identity}).plot({
+ opacity: {type: "ordinal", legend: "ramp"}
+ });
+}
+
+export async function ordinalOpacityThreshold() {
+ return Plot.cellX(d3.range(10), {fill: "red", opacity: Plot.identity}).plot({
+ opacity: {type: "threshold", legend: true, domain: [2, 5, 8], range: [0.2, 0.4, 0.6, 0.8]}
+ });
+}