// https://observablehq.com/@d3/color-legend@808 function _1(md){return( md`# Color Legend A simple legend for a [color scale](/@d3/color-schemes). Supports [continuous](/@d3/continuous-scales), [sequential](/@d3/sequential-scales), [diverging](/@d3/diverging-scales), [quantize, quantile, threshold](/@d3/quantile-quantize-and-threshold-scales) and [ordinal](/@d3/d3-scaleordinal) scales. To use: ~~~js import {Legend, Swatches} from "@d3/color-legend" ~~~ Then call the legend function as shown below. (For ordinal scales, also consider the swatches function.)` )} function _2(Legend,d3){return( Legend(d3.scaleSequential([0, 100], d3.interpolateViridis), { title: "Temperature (°F)" }) )} function _3(Legend,d3){return( Legend(d3.scaleSequentialSqrt([0, 1], d3.interpolateTurbo), { title: "Speed (kts)" }) )} function _4(Legend,d3){return( Legend(d3.scaleDiverging([-0.1, 0, 0.1], d3.interpolatePiYG), { title: "Daily change", tickFormat: "+%" }) )} function _5(Legend,d3){return( Legend(d3.scaleDivergingSqrt([-0.1, 0, 0.1], d3.interpolateRdBu), { title: "Daily change", tickFormat: "+%" }) )} function _6(Legend,d3){return( Legend(d3.scaleSequentialLog([1, 100], d3.interpolateBlues), { title: "Energy (joules)", ticks: 10 }) )} function _7(Legend,d3){return( Legend(d3.scaleSequentialQuantile(d3.range(100).map(() => Math.random() ** 2), d3.interpolateBlues), { title: "Quantile", tickFormat: ".2f" }) )} function _8(Legend,d3){return( Legend(d3.scaleSqrt([-100, 0, 100], ["blue", "white", "red"]), { title: "Temperature (°C)" }) )} function _9(Legend,d3){return( Legend(d3.scaleQuantize([1, 10], d3.schemePurples[9]), { title: "Unemployment rate (%)" }) )} function _10(Legend,d3){return( Legend(d3.scaleQuantile(d3.range(1000).map(d3.randomNormal(100, 20)), d3.schemeSpectral[9]), { title: "Height (cm)", tickFormat: ".0f" }) )} function _11(Legend,d3){return( Legend(d3.scaleThreshold([2.5, 3.1, 3.5, 3.9, 6, 7, 8, 9.5], d3.schemeRdBu[9]), { title: "Unemployment rate (%)", tickSize: 0 }) )} function _12(Legend,d3){return( Legend(d3.scaleOrdinal(["<10", "10-19", "20-29", "30-39", "40-49", "50-59", "60-69", "70-79", "≥80"], d3.schemeSpectral[10]), { title: "Age (years)", tickSize: 0 }) )} function _13(md){return( md`But wait, there’s more! How about swatches for ordinal color scales? Both variable-width swatches and [column layout](https://developer.mozilla.org/en-US/docs/Web/CSS/columns) are supported.` )} function _14(Swatches,d3){return( Swatches(d3.scaleOrdinal(["blueberries", "oranges", "apples"], d3.schemeCategory10)) )} function _15(Swatches,d3){return( Swatches(d3.scaleOrdinal(["Wholesale and Retail Trade", "Manufacturing", "Leisure and hospitality", "Business services", "Construction", "Education and Health", "Government", "Finance", "Self-employed", "Other"], d3.schemeTableau10), { columns: "180px" }) )} function _16(md){return( md`--- ## Implementation` )} function _Legend(d3){return( function Legend(color, { title, tickSize = 6, width = 320, height = 44 + tickSize, marginTop = 18, marginRight = 0, marginBottom = 16 + tickSize, marginLeft = 0, ticks = width / 64, tickFormat, tickValues } = {}) { function ramp(color, n = 256) { const canvas = document.createElement("canvas"); canvas.width = n; canvas.height = 1; const context = canvas.getContext("2d"); for (let i = 0; i < n; ++i) { context.fillStyle = color(i / (n - 1)); context.fillRect(i, 0, 1, 1); } return canvas; } const svg = d3.create("svg") .attr("width", width) .attr("height", height) .attr("viewBox", [0, 0, width, height]) .style("overflow", "visible") .style("display", "block"); let tickAdjust = g => g.selectAll(".tick line").attr("y1", marginTop + marginBottom - height); let x; // Continuous if (color.interpolate) { const n = Math.min(color.domain().length, color.range().length); x = color.copy().rangeRound(d3.quantize(d3.interpolate(marginLeft, width - marginRight), n)); svg.append("image") .attr("x", marginLeft) .attr("y", marginTop) .attr("width", width - marginLeft - marginRight) .attr("height", height - marginTop - marginBottom) .attr("preserveAspectRatio", "none") .attr("xlink:href", ramp(color.copy().domain(d3.quantize(d3.interpolate(0, 1), n))).toDataURL()); } // Sequential else if (color.interpolator) { x = Object.assign(color.copy() .interpolator(d3.interpolateRound(marginLeft, width - marginRight)), {range() { return [marginLeft, width - marginRight]; }}); svg.append("image") .attr("x", marginLeft) .attr("y", marginTop) .attr("width", width - marginLeft - marginRight) .attr("height", height - marginTop - marginBottom) .attr("preserveAspectRatio", "none") .attr("xlink:href", ramp(color.interpolator()).toDataURL()); // scaleSequentialQuantile doesn’t implement ticks or tickFormat. if (!x.ticks) { if (tickValues === undefined) { const n = Math.round(ticks + 1); tickValues = d3.range(n).map(i => d3.quantile(color.domain(), i / (n - 1))); } if (typeof tickFormat !== "function") { tickFormat = d3.format(tickFormat === undefined ? ",f" : tickFormat); } } } // Threshold else if (color.invertExtent) { const thresholds = color.thresholds ? color.thresholds() // scaleQuantize : color.quantiles ? color.quantiles() // scaleQuantile : color.domain(); // scaleThreshold const thresholdFormat = tickFormat === undefined ? d => d : typeof tickFormat === "string" ? d3.format(tickFormat) : tickFormat; x = d3.scaleLinear() .domain([-1, color.range().length - 1]) .rangeRound([marginLeft, width - marginRight]); svg.append("g") .selectAll("rect") .data(color.range()) .join("rect") .attr("x", (d, i) => x(i - 1)) .attr("y", marginTop) .attr("width", (d, i) => x(i) - x(i - 1)) .attr("height", height - marginTop - marginBottom) .attr("fill", d => d); tickValues = d3.range(thresholds.length); tickFormat = i => thresholdFormat(thresholds[i], i); } // Ordinal else { x = d3.scaleBand() .domain(color.domain()) .rangeRound([marginLeft, width - marginRight]); svg.append("g") .selectAll("rect") .data(color.domain()) .join("rect") .attr("x", x) .attr("y", marginTop) .attr("width", Math.max(0, x.bandwidth() - 1)) .attr("height", height - marginTop - marginBottom) .attr("fill", color); tickAdjust = () => {}; } svg.append("g") .attr("transform", `translate(0,${height - marginBottom})`) .call(d3.axisBottom(x) .ticks(ticks, typeof tickFormat === "string" ? tickFormat : undefined) .tickFormat(typeof tickFormat === "function" ? tickFormat : undefined) .tickSize(tickSize) .tickValues(tickValues)) .call(tickAdjust) .call(g => g.select(".domain").remove()) .call(g => g.append("text") .attr("x", marginLeft) .attr("y", marginTop + marginBottom - height - 6) .attr("fill", "currentColor") .attr("text-anchor", "start") .attr("font-weight", "bold") .attr("class", "title") .text(title)); return svg.node(); } )} function _legend(Legend){return( function legend({color, ...options}) { return Legend(color, options); } )} function _Swatches(d3,htl){return( function Swatches(color, { columns = null, format, unknown: formatUnknown, swatchSize = 15, swatchWidth = swatchSize, swatchHeight = swatchSize, marginLeft = 0 } = {}) { const id = `-swatches-${Math.random().toString(16).slice(2)}`; const unknown = formatUnknown == null ? undefined : color.unknown(); const unknowns = unknown == null || unknown === d3.scaleImplicit ? [] : [unknown]; const domain = color.domain().concat(unknowns); if (format === undefined) format = x => x === unknown ? formatUnknown : x; function entity(character) { return `&#${character.charCodeAt(0).toString()};`; } if (columns !== null) return htl.html`
${domain.map(value => { const label = `${format(value)}`; return htl.html`
${label}
`; })}
`; return htl.html`
${domain.map(value => htl.html`${format(value)}`)}
`; } )} function _swatches(Swatches){return( function swatches({color, ...options}) { return Swatches(color, options); } )} export default function define(runtime, observer) { const main = runtime.module(); main.variable(observer()).define(["md"], _1); main.variable(observer()).define(["Legend","d3"], _2); main.variable(observer()).define(["Legend","d3"], _3); main.variable(observer()).define(["Legend","d3"], _4); main.variable(observer()).define(["Legend","d3"], _5); main.variable(observer()).define(["Legend","d3"], _6); main.variable(observer()).define(["Legend","d3"], _7); main.variable(observer()).define(["Legend","d3"], _8); main.variable(observer()).define(["Legend","d3"], _9); main.variable(observer()).define(["Legend","d3"], _10); main.variable(observer()).define(["Legend","d3"], _11); main.variable(observer()).define(["Legend","d3"], _12); main.variable(observer()).define(["md"], _13); main.variable(observer()).define(["Swatches","d3"], _14); main.variable(observer()).define(["Swatches","d3"], _15); main.variable(observer()).define(["md"], _16); main.variable(observer("Legend")).define("Legend", ["d3"], _Legend); main.variable(observer("legend")).define("legend", ["Legend"], _legend); main.variable(observer("Swatches")).define("Swatches", ["d3","htl"], _Swatches); main.variable(observer("swatches")).define("swatches", ["Swatches"], _swatches); return main; }