Spaces:
Running
Running
| // 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`<div style="display: flex; align-items: center; margin-left: ${+marginLeft}px; min-height: 33px; font: 10px sans-serif;"> | |
| <style> | |
| .${id}-item { | |
| break-inside: avoid; | |
| display: flex; | |
| align-items: center; | |
| padding-bottom: 1px; | |
| } | |
| .${id}-label { | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| max-width: calc(100% - ${+swatchWidth}px - 0.5em); | |
| } | |
| .${id}-swatch { | |
| width: ${+swatchWidth}px; | |
| height: ${+swatchHeight}px; | |
| margin: 0 0.5em 0 0; | |
| } | |
| </style> | |
| <div style=${{width: "100%", columns}}>${domain.map(value => { | |
| const label = `${format(value)}`; | |
| return htl.html`<div class=${id}-item> | |
| <div class=${id}-swatch style=${{background: color(value)}}></div> | |
| <div class=${id}-label title=${label}>${label}</div> | |
| </div>`; | |
| })} | |
| </div> | |
| </div>`; | |
| return htl.html`<div style="display: flex; align-items: center; min-height: 33px; margin-left: ${+marginLeft}px; font: 10px sans-serif;"> | |
| <style> | |
| .${id} { | |
| display: inline-flex; | |
| align-items: center; | |
| margin-right: 1em; | |
| } | |
| .${id}::before { | |
| content: ""; | |
| width: ${+swatchWidth}px; | |
| height: ${+swatchHeight}px; | |
| margin-right: 0.5em; | |
| background: var(--color); | |
| } | |
| </style> | |
| <div>${domain.map(value => htl.html`<span class="${id}" style="--color: ${color(value)}">${format(value)}</span>`)}</div>`; | |
| } | |
| )} | |
| 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; | |
| } | |