-
Couldn't load subscription status.
- Fork 197
legends - 2 #583
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
legends - 2 #583
Changes from 14 commits
8601acc
9f3d9cd
3398e54
d1b9b81
dd32499
dbd57f1
0c7f150
2096b24
9f85747
b4d8a33
504fb45
5feae2e
e14cff8
40da320
61257a7
8803da9
3275764
087d7ad
013a1ff
729b4a2
1d700f6
c226965
7bec561
20ae131
47da1cb
b6f3d92
53c8108
e285768
ff9669c
f7d33b3
7eb7d0a
0630304
c5be06d
e834bab
d744483
a71a2c0
8cef472
12e0e47
e7000c9
7791d3e
a92d7da
3ecdcb4
72e5278
6bafb92
9630852
e85d7e1
9f01a29
ad22830
dd53c0c
f9a3617
a09d7c0
6ccf13f
6fb58a9
5e6ef47
ff394be
a3c14c6
453bfb4
dcc2807
ee294c4
592ab4a
9f0fc76
6e1b5e8
a2ca185
8f02d27
81583bb
d0c213e
e458581
740605f
6db63fc
952fc08
cc1d36e
c748543
d3cd390
9fc55ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
|
|
||
| // Wrap the plot in a figure with a caption, if desired. | ||
| export function figureWrap(svg, {width}, caption) { | ||
| if (caption == null) return svg; | ||
| const figure = document.createElement("figure"); | ||
| figure.style = `max-width: ${width}px`; | ||
| figure.appendChild(svg); | ||
| const figcaption = document.createElement("figcaption"); | ||
| figcaption.appendChild(caption instanceof Node ? caption : document.createTextNode(caption)); | ||
| figure.appendChild(figcaption); | ||
| return figure; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import {legendColor} from "./legends/color.js"; | ||
|
|
||
| export function legend({color, ...options}) { | ||
| if (color) return legendColor({...color, ...options}); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import {Scale} from "../scales.js"; | ||
| import {legendRamp} from "./ramp.js"; | ||
| import {legendSwatches} from "./swatches.js"; | ||
|
|
||
| export function legendColor({legend, ...options}) { | ||
| const scale = Scale("color", undefined, options); | ||
| if (legend === undefined) legend = scale.type === "ordinal" || scale.type === "categorical" ? "swatches" : "ramp"; | ||
| switch (legend) { | ||
| case "swatches": | ||
| return legendSwatches({...scale, ...options}); | ||
| case "ramp": | ||
| return legendRamp({...scale, ...options}); | ||
| default: | ||
| throw new Error(`unknown legend type ${legend}`); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| import {create, scaleLinear, quantize, interpolate, interpolateRound, quantile, range, format, scaleBand, axisBottom} from "d3"; | ||
|
|
||
| export function legendRamp({ | ||
| label, | ||
| tickSize = 6, | ||
| width = 240, | ||
| height = 44 + tickSize, | ||
| marginTop = 18, | ||
| marginRight = 0, | ||
| marginBottom = 16 + tickSize, | ||
| marginLeft = 0, | ||
| ticks = width / 64, | ||
| tickFormat, | ||
| tickValues, | ||
| scale: color | ||
| } = {}) { | ||
| const svg = 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(quantize(interpolate(marginLeft, width - marginRight), n)); | ||
| let color2 = color.copy().domain(quantize(interpolate(0, 1), n)); | ||
| // special case for log scales | ||
| if (color.base) { | ||
| const p = scaleLinear( | ||
| quantize(interpolate(0, 1), color.domain().length), | ||
| color.domain().map(d => Math.log(d)) | ||
| ); | ||
| color2 = t => color(Math.exp(p(t))); | ||
| } | ||
| 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(color2).toDataURL()); | ||
| } | ||
|
|
||
| // Sequential | ||
| else if (color.interpolator) { | ||
| x = Object.assign(color.copy() | ||
| .interpolator(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 = range(n).map(i => quantile(color.domain(), i / (n - 1))); | ||
| } | ||
| if (typeof tickFormat !== "function") { | ||
| tickFormat = 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" ? format(tickFormat) | ||
| : tickFormat; | ||
|
|
||
| x = 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 = range(thresholds.length); | ||
| tickFormat = i => thresholdFormat(thresholds[i], i); | ||
| } | ||
|
|
||
| // Ordinal | ||
| else { | ||
| x = 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(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(label === undefined ? () => {} | ||
| : 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", "label") | ||
| .text(label)); | ||
|
|
||
| return svg.node(); | ||
| } | ||
|
|
||
| function ramp(color, n = 256) { | ||
| const canvas = create("canvas").attr("width", n).attr("height", 1).node(); | ||
| 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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import {create} from "d3"; | ||
| import {maybeClassName} from "../style.js"; | ||
|
|
||
| // TODO: once we inline, is this smart variable handling any | ||
| // better than inline styles? | ||
| const styles = uid => ` | ||
| .${uid} { | ||
| display: flex; | ||
| align-items: center; | ||
| margin-left: var(--marginLeft); | ||
| min-height: 33px; | ||
| font: 10px sans-serif; | ||
| margin-bottom: 0.5em; | ||
| } | ||
|
|
||
| .${uid} > div { | ||
| width: 100%; | ||
| } | ||
|
|
||
| .${uid} .swatch-item { | ||
| break-inside: avoid; | ||
| display: flex; | ||
| align-items: center; | ||
| padding-bottom: 1px; | ||
| } | ||
|
|
||
| .${uid} .swatch-label { | ||
| white-space: nowrap; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| max-width: calc(100% - var(--swatchWidth) - 0.5em); | ||
| } | ||
|
|
||
| .${uid} .swatch-block { | ||
| width: var(--swatchWidth); | ||
| height: var(--swatchHeight); | ||
| margin: 0 0.5em 0 0; | ||
| } | ||
|
|
||
| .${uid} .plot-swatch { | ||
Fil marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| display: inline-flex; | ||
| align-items: center; | ||
| margin-right: 1em; | ||
| } | ||
|
|
||
| .${uid} .plot-swatch::before { | ||
| content: ""; | ||
| width: var(--swatchWidth); | ||
| height: var(--swatchHeight); | ||
| margin-right: 0.5em; | ||
| background: var(--color); | ||
| } | ||
| `; | ||
|
|
||
| export function legendSwatches({ | ||
| columns = null, | ||
| format = x => x, | ||
| label, | ||
| swatchSize = 15, | ||
| swatchWidth = swatchSize, | ||
| swatchHeight = swatchSize, | ||
| marginLeft = 0, | ||
| className, | ||
| uid = maybeClassName(className), | ||
| style = styles(uid), | ||
| width, | ||
| scale: color | ||
| } = {}) { | ||
| const swatches = create("div") | ||
| .classed(uid, true) | ||
| .attr("style", `--marginLeft: ${+marginLeft}px; --swatchWidth: ${+swatchWidth}px; --swatchHeight: ${+swatchHeight}px;${ | ||
| width === undefined ? "" : ` width: ${width}px;` | ||
| }`); | ||
| swatches.append("style").text(style); | ||
|
|
||
| if (columns !== null) { | ||
| const elems = swatches.append("div") | ||
| .style("columns", columns); | ||
| for (const value of color.domain()) { | ||
| const d = elems.append("div").classed("swatch-item", true); | ||
| d.append("div") | ||
| .classed("swatch-block", true) | ||
| .style("background", color(value)); | ||
| const label = format(value); | ||
| d.append("div") | ||
| .classed("swatch-label", true) | ||
| .text(label) | ||
| .attr("title", label.replace(/["&]/g, entity)); | ||
|
||
| } | ||
| } else { | ||
| swatches | ||
| .selectAll() | ||
| .data(color.domain()) | ||
| .join("span") | ||
| .classed("plot-swatch", true) | ||
| .style("--color", color) | ||
| .text(format); | ||
| } | ||
|
|
||
| return label == null | ||
| ? swatches.node() | ||
| : create("div") | ||
| .call(div => div.append("div") | ||
| .style("font-weight", "bold") | ||
| .style("font-family", "sans-serif") | ||
| .style("font-size", "10px") | ||
| .style("margin", "5px 0 -5px 0") | ||
| .text(label)) | ||
| .call(div => div.append(() => swatches.node())) | ||
| .node(); | ||
| } | ||
|
|
||
| function entity(character) { | ||
| return `&#${character.charCodeAt(0).toString()};`; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This appears to be sneaking in a fix for #356? I’m going to remove this; I’d rather address it separately.