-
Notifications
You must be signed in to change notification settings - Fork 31
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
New component: Color scale legend #121
base: main
Are you sure you want to change the base?
Conversation
Here are some demos: |
Thanks for putting this together! I'll go through it. How would you feel about converting it from TypeScript to using JsDoc comments like the other components? I'd also be open to having two versions available – the website would need to be updated to support that. Or, the lowest overhead way would be to keep it in the repo and link to the second version. |
Oops, didn't see the code base uses JsDoc. No problem, I'll convert. |
add CSS variable font-size: var(--cbar-font-size, 10pt);
@mhkeller Let me know if this needs further work. |
Thanks a bunch for your work on this. I’ll go through it in more detail. One thing would be to not load the d3 scale chromatic library separately. Instead it should be more like the key component https://layercake.graphics/components/Key.html.svelte where it uses the z scale. I’d probably get rid of the wrapperStyle prop but open to hear why it should stay. My thinking is that the user could either manually add styles to this component’s css or easily add this prop based on their own reusability/customization needs. It may be that they would prefer to do that via a class or just one css rule like a float. In short, there’s a lot of ways the user could handle custom styling and it may be better to let them implement that. There’s a TODO on the vertical layout. Is that finished? |
The style and textStyle props may fall in that same bucket too |
I did a first pass of making this more layercake-y – mostly where the scale is set via the context and it uses the scale to determine ticks. I copied the logic from the x-axis component and added the I set up a page in the component gallery: It would be nice to add a few more styling and configuration options. Maybe with some checkbox buttons to make it interactive. Let me know what you think or if I got rid of something important. Do you think the |
Looks great! Sorry for not responding earlier. I just looked at your changes. All looks good. Nothing important was removed. I do have several use cases for vertical color bars and don't think it takes that much more work to implement it but I'm on a deadline atm. So maybe leave it for another day. I added a bit of interactivity to the demo page you made. Nice work on that! The form controls I added show how to use the CSS variables you asked about. Screen.Recording.2023-03-25.at.12.32.50.mp4 |
Very cool! Whenever you have the time to work on the vertical bar that would be great. I'd take a crack at it but I feel like you have your CSS system set up a certain way. I'd like to add more interactivity for the other components too so this is a good start. I'd maybe add some checkboxes showing the name or something but we can figure all that out later. Here are the remaining TODOs
|
I added a tick mark option as well as a I'm still not sure what the best way is to dynamically convert this to a vertical layout. With the ticks and labels, it may be easier if that is a separate component. If you see a simple way to make it all work together, though, let me know. |
I need to make the sliders nicer / better organized on the demo page. |
@janosh I reconfigured it quite a bit with a few different flex boxes so that the user could pick any combination of tick side top or bottom and label side and not have the text elements crash into one another. I'm sure it could be improved though, so let me know what you think. The added complexity makes me think that adding a vertical layout option for this will be a bit too much. Perhaps that's just an easy separate component. |
Thanks for taking a look! Totally on the gallery view – I have just been looking at it on the full page. I need to design all of the sliders and things so they fit properly. I really like your technique of assigning css variables to components as extra props – I had never thought of that and it's very cool so I'd like to definitely include the width as an example of that. On the label, I thought I had fixed that with this one: 8e34d8b I'll take another look as to why that's not working |
By the way, I increased the ramp value to |
That's good to know!
I'm a big fan of that Svelte feature too! The only thing I don't like about it is that it relies on wrapping the component in an extra |
Brave on macOS here. So Chromium based. |
I think I fixed it. I'm sure the CSS logic could be improved... |
What kind of control widget would you think would be best for that? Maybe the simplest would be like "Generate random range" ? You mean you want something where instead of Also, if you have any thoughts on how to shrink these controls down and make them look more like a control box that would be great. Also if you see any improvements to the CSS structure let me know too. I changed it from points to pixels just to be consistent with the other examples. But I could see parameterizing the distance the tick labels are from the bar, too. Thanks for your help with this! |
Exactly! I think two
I think the controls look great already. No changes needed IMO. I'll take a look at the CSS and let you know. |
Maybe the lightest touch solution would be to start at |
Hadn't noticed https://github.com/mhkeller/svelte-double-range-slider yet. Nice work! Just had another look at ColorBar and format on save applied a bunch of changes. But nothing jumps out as easily simplified in the CSS. |
? ticks($zScale.ticks()) | ||
: $zScale.ticks(ticks); | ||
|
||
$: ramped = [...Array(steps).keys()].map((i) => $zScale(i / steps)); |
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.
@janosh I think the gradient creation needs some tweaks. I was testing it out with scaleDiverging
using a domain of [-1, 0, 1]
and because the gradient uses i
here, it will always start at 0
and never get negative values. It should instead use the tick values. This should be a good example to work from: https://observablehq.com/@d3/color-legend
hey @janosh wanted to see if you were still interested in this one |
Absolutely. Sorry about the radio silence. Great catch re diverging color scales. If we're just interested in fixing that issue, we could use this? function ramp(scale, steps = 100) {
const domain = scale.domain()
const step = (domain.at(-1) - domain[0]) / (steps - 1)
return [...Array(steps).keys()].map((idx) => scale(domain[0] + idx * step))
} |
Hm yea I think that could work. I'll look at what this observable notebook does, too. I think just using whatever the canonical d3 example uses would be best in case there are other use cases or edge cases. |
@mhkeller I looked into that as well. It's possible to just wrap the code Legend.svelte<script lang="ts">
import * as d3 from 'd3'
import { onMount } from 'svelte'
import { pretty_num } from '../lib/labels'
export let color: d3.ScaleSequential<string> | d3.ScaleSequential<number>
export let title = ''
export let tickSize = 6
export let width = 400
export let height = 50 + tickSize
export let marginTop = 20
export let marginRight = 0
export let marginBottom = 20 + tickSize
export let marginLeft = 0
export let ticks = width / 100
export let tick_vals: number[] | null = null
export let node: SVGElement | null = null
function ramp(color, samples: Number = 100) {
const canvas = document.createElement('canvas')
canvas.width = samples
canvas.height = 1
const context = canvas.getContext('2d')
for (let idx = 0; idx < samples; ++idx) {
context.fillStyle = color(idx / (samples - 1))
context.fillRect(idx, 0, 1, 1)
}
return canvas
}
onMount(async () => {
const svg = d3
.select(node)
.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())
}
// Threshold
else if (color.invertExtent) {
const thresholds = color.thresholds
? color.thresholds() // scaleQuantize
: color.quantiles
? color.quantiles() // scaleQuantile
: color.domain() // scaleThreshold
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)
tick_vals = d3.range(thresholds.length)
}
// 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, pretty_num)
.tickFormat(pretty_num)
.tickSize(tickSize)
.tickValues(tick_vals)
)
.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')
.text(title)
)
})
</script>
<svg bind:this={node} class="legend" /> where import { format } from 'd3-format'
export const pretty_num = (num: number, precision?: string) => {
if (num === null) return ``
if (!precision) {
const [gt_1_fmt, lt_1_fmt] = default_precision
return format(Math.abs(num) >= 1 ? gt_1_fmt : lt_1_fmt)(num)
}
return format(precision)(num)
} But it's not the best UX. If you load 20 or so on the same page, they're empty at first and then flash into existence, causing a lot of CLS. |
For sure. I don't think we need to go the full |
Sorry I'm jumping in late on the discussion. Not directly related to this PR, but more general color scale / color bar notes (and can create a separate issue if we want to continue the discussion) @mhkeller What do you think if we add a Also, LayerChart has Legend and ColorRamp components , which are heavily inspired by https://observablehq.com/@d3/color-legend and https://clhenrick.github.io/color-legend-element/. There is additional work planned: techniq/layerchart#22. I only mention it in case it helps with this PR (see approach, etc). |
Hm this notebook no longer exists: https://observablehq.com/@d3/color-legend |
Weird. Same now... |
Looking at https://observablehq.com/@d3/color-legend, there are a lot of different types of scales that are supported here that each require their own implementation. I'm not sure this will be a big priority for me in the near-term but if someone wants to take this up, that would be great. |
src/_components/ColorBar.svelte
Here's a rough 1st draft for closing #120. Feel free to suggest or apply yourself substantial changes.