-
Notifications
You must be signed in to change notification settings - Fork 186
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
Merged
Merged
legends - 2 #583
Changes from 60 commits
Commits
Show all changes
74 commits
Select commit
Hold shift + click to select a range
8601acc
legends
Fil 9f3d9cd
Plot.legend takes a scale and options
Fil 3398e54
Remove Plot.legend, use chart.legend instead.
Fil d1b9b81
reinstate Plot.legend
Fil dd32499
unused code
Fil dbd57f1
allow legend: "ramp" as an option
Fil 0c7f150
snapshot test for a lot of color legends
Fil 2096b24
no random in unit tests
Fil 9f85747
remove redundant color tests
Fil b4d8a33
accept a label on swatches
Fil 504fb45
className for legendSwatches
Fil 5feae2e
remove radius and opacity legends for now
Fil e14cff8
more scope
Fil 40da320
only color is available in this branch
Fil 61257a7
do not expose any class
Fil 8803da9
error on unknown legend type
mbostock 3275764
categorical is normalized to ordinal
mbostock 087d7ad
show unknown legend type in error
mbostock 013a1ff
prEtTieR
mbostock 729b4a2
prioritize type; avoid unnecessary default
mbostock 1d700f6
non-nullish, not truthy
mbostock c226965
div.append(…nodes)
mbostock 7bec561
legends
Fil 20ae131
Plot.legend takes a scale and options
Fil 47da1cb
Remove Plot.legend, use chart.legend instead.
Fil b6f3d92
reinstate Plot.legend
Fil 53c8108
unused code
Fil e285768
allow legend: "ramp" as an option
Fil ff9669c
snapshot test for a lot of color legends
Fil f7d33b3
no random in unit tests
Fil 7eb7d0a
remove redundant color tests
Fil 0630304
accept a label on swatches
Fil c5be06d
className for legendSwatches
Fil e834bab
remove radius and opacity legends for now
Fil d744483
more scope
Fil a71a2c0
only color is available in this branch
Fil 8cef472
do not expose any class
Fil 12e0e47
error on unknown legend type
mbostock e7000c9
categorical is normalized to ordinal
mbostock 7791d3e
show unknown legend type in error
mbostock a92d7da
prEtTieR
mbostock 3ecdcb4
prioritize type; avoid unnecessary default
mbostock 72e5278
non-nullish, not truthy
mbostock 6bafb92
div.append(…nodes)
mbostock 9630852
less duck typing
Fil e85d7e1
remove entity filtering
Fil 9f01a29
document chart.legend and Plot.legend
Fil ad22830
reduce duck-typing
Fil dd53c0c
add a test for Plot.legend with options
Fil f9a3617
styles
Fil a09d7c0
clear up some confusion between scale options and legend options
Fil 6ccf13f
className
Fil 6fb58a9
apply() rather than color()
Fil 5e6ef47
Update README
mbostock ff394be
revert figure changes
mbostock a3c14c6
avoid closure
mbostock 453bfb4
applyInlineStyles
mbostock dcc2807
revert diverging scale changes
mbostock ee294c4
Merge branch 'main' into fil/legends-2
mbostock 592ab4a
inline styles; fix diverging; separate tests
mbostock 9f0fc76
stringify and lowercase legend option
mbostock 6e1b5e8
normalizeScale
mbostock a2ca185
inherit scale options
mbostock 8f02d27
fix ordinal tickFormat function
mbostock 81583bb
explicit ordinal ticks
mbostock d0c213e
use pushState for tests
mbostock e458581
round option
mbostock 740605f
fix for truncated schemes
mbostock 6db63fc
opacity legend (#587)
Fil 952fc08
legend: true
mbostock cc1d36e
fix test determinism
mbostock c748543
fix inline opacity legends
mbostock d3cd390
arrow key navigation
mbostock 9fc55ab
ignore style if null
mbostock File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import {exposeScale, Scale} from "./scales.js"; | ||
import {legendColor} from "./legends/color.js"; | ||
|
||
export function legend({color, ...options}) { | ||
if (color != null) { | ||
return legendColor(exposeScale(Scale("color", undefined, color)), { | ||
// ...color, | ||
label: color.label, | ||
// ticks: color.ticks, // maybe? | ||
// tickFormat: color.tickFormat, // maybe? | ||
// tickValues: color.tickValues, // maybe? | ||
// format: color.format, // maybe? | ||
...options | ||
}); | ||
} | ||
throw new Error(`unsupported legend type`); | ||
} | ||
|
||
export function exposeLegends(type, options) { | ||
return legend({...options, [type]: this.scale(type)}); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import {legendRamp} from "./ramp.js"; | ||
import {legendSwatches} from "./swatches.js"; | ||
|
||
export function legendColor(color, {legend, ...options}) { | ||
if (legend === undefined) legend = color.type === "ordinal" ? "swatches" : "ramp"; | ||
switch (legend) { | ||
case "swatches": | ||
return legendSwatches(color, options); | ||
case "ramp": | ||
return legendRamp(color, options); | ||
default: | ||
throw new Error(`unknown color legend type: ${legend}`); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
import {Scale} from "../scales.js"; | ||
import {create, quantize, interpolateNumber, piecewise, format, scaleBand, scaleLinear, axisBottom} from "d3"; | ||
import {applyInlineStyles, maybeClassName} from "../style.js"; | ||
|
||
export function legendRamp(scale, { | ||
label, | ||
tickSize = 6, | ||
width = 240, | ||
height = 44 + tickSize, | ||
marginTop = 18, | ||
marginRight = 0, | ||
marginBottom = 16 + tickSize, | ||
marginLeft = 0, | ||
style, | ||
ticks = width / 64, | ||
tickFormat, | ||
tickValues, | ||
className | ||
}) { | ||
className = maybeClassName(className); | ||
|
||
const svg = create("svg") | ||
.attr("class", className) | ||
.attr("font-family", "system-ui, sans-serif") | ||
.attr("font-size", 10) | ||
.attr("font-variant", "tabular-nums") | ||
.attr("width", width) | ||
.attr("height", height) | ||
.attr("viewBox", `0 0 ${width} ${height}`) | ||
.call(svg => svg.append("style").text(` | ||
.${className} { | ||
display: block; | ||
background: white; | ||
height: auto; | ||
height: intrinsic; | ||
max-width: 100%; | ||
overflow: visible; | ||
} | ||
.${className} text { | ||
white-space: pre; | ||
} | ||
`)) | ||
.call(applyInlineStyles, style); | ||
|
||
let tickAdjust = g => g.selectAll(".tick line").attr("y1", marginTop + marginBottom - height); | ||
let x; | ||
|
||
const {type, domain, range, interpolate, apply} = scale; | ||
|
||
// Continuous | ||
if (interpolate) { | ||
|
||
// Often interpolate is a “fixed” interpolator on the [0, 1] interval, as | ||
// with a built-in color scheme, but sometimes it is a function that takes | ||
// two arguments and is used in conjunction with the range. | ||
const interpolator = interpolate.length === 1 | ||
? interpolate | ||
: piecewise(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. | ||
x = Scale("color", undefined, scale).scale.rangeRound( | ||
quantize( | ||
interpolateNumber(marginLeft, width - marginRight), | ||
Math.min( | ||
domain.length + (scale.pivot !== undefined), | ||
range === undefined ? Infinity : range.length | ||
) | ||
) | ||
); | ||
|
||
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(interpolator).toDataURL()); | ||
} | ||
|
||
// Threshold | ||
else if (type === "threshold") { | ||
const thresholds = domain; | ||
|
||
const thresholdFormat | ||
= tickFormat === undefined ? d => d | ||
: typeof tickFormat === "string" ? format(tickFormat) | ||
: tickFormat; | ||
|
||
// Construct a linear scale with evenly-spaced ticks for each of the | ||
// thresholds; the domain extends one beyond the threshold extent. | ||
x = scaleLinear() | ||
.domain([-1, range.length - 1]) | ||
.rangeRound([marginLeft, width - marginRight]); | ||
|
||
svg.append("g") | ||
.selectAll("rect") | ||
.data(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 = Array.from(thresholds, (_, i) => i); | ||
tickFormat = i => thresholdFormat(thresholds[i], i); | ||
} | ||
|
||
// Ordinal (hopefully!) | ||
else { | ||
x = scaleBand() | ||
.domain(domain) | ||
.rangeRound([marginLeft, width - marginRight]); | ||
|
||
svg.append("g") | ||
.selectAll("rect") | ||
.data(domain) | ||
.join("rect") | ||
.attr("x", x) | ||
.attr("y", marginTop) | ||
.attr("width", Math.max(0, x.bandwidth() - 1)) | ||
.attr("height", height - marginTop - marginBottom) | ||
.attr("fill", apply); | ||
|
||
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)) | ||
.attr("font-size", null) | ||
.attr("font-family", null) | ||
.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") // TODO move to stylesheet? | ||
.attr("text-anchor", "start") | ||
.attr("font-weight", "bold") | ||
.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; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import {create} from "d3"; | ||
import {applyInlineStyles, maybeClassName} from "../style.js"; | ||
|
||
export function legendSwatches(color, { | ||
columns, | ||
format = x => x, | ||
// TODO label, | ||
swatchSize = 15, | ||
swatchWidth = swatchSize, | ||
swatchHeight = swatchSize, | ||
marginLeft = 0, | ||
className, | ||
style, | ||
width | ||
} = {}) { | ||
className = maybeClassName(className); | ||
|
||
const swatches = create("div") | ||
.attr("class", className) | ||
.attr("style", ` | ||
--swatchWidth: ${+swatchWidth}px; | ||
--swatchHeight: ${+swatchHeight}px; | ||
`); | ||
|
||
let extraStyle; | ||
|
||
if (columns != null) { | ||
extraStyle = ` | ||
.${className}-swatch { | ||
display: flex; | ||
align-items: center; | ||
break-inside: avoid; | ||
padding-bottom: 1px; | ||
} | ||
.${className}-swatch::before { | ||
flex-shrink: 0; | ||
} | ||
.${className}-label { | ||
white-space: nowrap; | ||
overflow: hidden; | ||
text-overflow: ellipsis; | ||
} | ||
`; | ||
|
||
swatches | ||
.style("columns", columns) | ||
.selectAll() | ||
.data(color.domain) | ||
.join("div") | ||
.attr("class", `${className}-swatch`) | ||
.style("--color", color.apply) | ||
.call(item => item.append("div") | ||
.attr("class", `${className}-label`) | ||
.attr("title", format) | ||
.text(format)); | ||
} else { | ||
extraStyle = ` | ||
.${className} { | ||
display: flex; | ||
align-items: center; | ||
min-height: 33px; | ||
} | ||
.${className}-swatch { | ||
display: inline-flex; | ||
align-items: center; | ||
margin-right: 1em; | ||
} | ||
`; | ||
|
||
swatches | ||
.selectAll() | ||
.data(color.domain) | ||
.join("span") | ||
.attr("class", `${className}-swatch`) | ||
.style("--color", color.apply) | ||
.text(format); | ||
} | ||
|
||
return swatches | ||
.call(div => div.insert("style", "*").text(` | ||
.${className} { | ||
font-family: system-ui, sans-serif; | ||
font-size: 10px; | ||
font-variant: tabular-nums; | ||
margin-bottom: 0.5em;${marginLeft === undefined ? "" : ` | ||
margin-left: ${+marginLeft}px;`}${width === undefined ? "" : ` | ||
width: ${width}px;`} | ||
} | ||
.${className}-swatch::before { | ||
content: ""; | ||
width: var(--swatchWidth); | ||
height: var(--swatchHeight); | ||
margin-right: 0.5em; | ||
background: var(--color); | ||
} | ||
${extraStyle} | ||
`)) | ||
.call(applyInlineStyles, style) | ||
.node(); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 will mutate color.type. 😬