Conversation
|
This implementation is feeling pretty good to me now, but I’m beginning to see what you were going after with #1210: currently there’s not an easy way for the Contour mark to leverage the nearest/barycentric/random-walk interpolation methods to convert non-gridded samples into a grid suitable for d3-contour. But clearly we would like to be able to use them e.g. to produce contours over the ca55 dataset. So I think what I’d like to do is change the rasterize function option to return a dense array of gridded values (in data space, without scaling), and probably rename it back to interpolate as we originally imagined. That’ll make things a little slower for the raster mark since it’s another copy of the data, and it means that the nearest interpolation method won’t be able to use the Canvas2D API to render, but I think the performance difference will probably be small enough that it won’t be noticeable, and it will allow the Contour mark to reuse these implementations. Let me know if that sounds good or please take a crack at it as you like, @Fil! |
| }, | ||
| marks: [ | ||
| Plot.contour({ | ||
| value: (x, y) => x * y * Math.sin(x) * Math.cos(y), |
There was a problem hiding this comment.
I feel like it would be more natural to specify fill here, rather than value. Maybe we can make that work as shorthand, but I didn’t want to support fill and stroke being set to different functions (multiple overlapping contours?). 🤔
There was a problem hiding this comment.
yes it would be very nice; let's just throw an error if both are functions?
There was a problem hiding this comment.
I will work on more shorthand tomorrow.
|
The contours for ca55 are very pretty with more blurring (e.g. blur: 3), and the barycentric interpolate doesn't have the arbitrary (random) high frequency component of random-walk. So I'd suggest to change the test to barycentric+blur: 3, to make it a better example. (DONE) The contours are empty if we don't specify an interpolate strategy; I think this would probably be addressed by d3/d3-contour#61: it would make contours around the samples. But I don't think that's what people will want in general (and certainly not on this dataset), and I'd suggest we either:
It would be good to backport this to d3-contour at some point, to allow operations on the returned array of GeoJSON? |
Contour blurring is unchanged, and blurs the abstract data (with a linear interpolation). Raster blurring is made with d3.blurImage. Two consequences: * we can now blur “categorical” colors, if we want to smooth out the image and give it a polished look in the higher variance regions. (This works very well when we have two colors, but with more categories there is a risk of hiding the components of a color, making the image more difficult to understand. Anyway, it’s available as an option to play with.) * for quantitative data, and with a color scale with continuous scheme and linear transform, this is very close to linear interpolation; but if the underlying data is better rendered with a log color scale, the color interpolation takes this into account (which IMO is better).
|
I think blurring + random-walk is also a good solution to #1196 (comment) (see dd06ea05c commit log). |
…nterpolate function, and ignore x and y filters on geometries
note: the penguins dataset is full of surprises since some points are occluded by others of a different species…
About the shorthand and defaults, I suggest we follow a similar convention as in Plot.density:
|
|
It's in a test I haven't pushed because it's very slow—I'll try to make a lighter one, but for now I've built 11dbb07 and added it to https://observablehq.com/@observablehq/global-temperatures-raster#issue11dbb0; you can see that it fails to create the contours. |
|
Ah no! It works—it's just that the defaults have changed 👍 |
|
I think there are still some issues with the concept of a “dense” grid, and in particular whether this is a linear grid in data space or in screen coordinates. For example, say we start here: Plot.plot({
marks: [Plot.raster(await vapor(), {width: 360, height: 180, fill: (d) => d})]
})We can fix the y inversion y reversing the y scale: Plot.plot({
y: {reverse: true},
marks: [Plot.raster(await vapor(), {width: 360, height: 180, fill: (d) => d})]
})But what if we wanted instead to specify the x domain as [-180, 180] and the y domain as [90, -90]? Then we get something surprising because the defaults for x1, y1, x2, y2 and 0, 0, width, height. Plot.plot({
x: {domain: [-180, 180]},
y: {domain: [90, -90]},
marks: [Plot.raster(await vapor(), {width: 360, height: 180, fill: (d) => d})]
})We can fix that by specifying these options explicitly, I suppose. Plot.plot({
x: {domain: [-180, 180]},
y: {domain: [90, -90]},
marks: [Plot.raster(await vapor(), {x1: -180, y1: -90, y2: 90, x2: 180, width: 360, height: 180, fill: (d) => d})]
})But notice what happens when we change the y scale type of sqrt: the raster stays the same, even though the axis has changed. Plot.plot({
y: {type: "sqrt"},
marks: [Plot.raster(await vapor(), {width: 360, height: 180, fill: (d) => d})]
})In other words, when you use the dense grid shorthand, you have to provide a dense grid in scaled (projected) coordinates, which makes it rather difficult to use. I get more what I expect if I specify x and y explicitly (non-optimized). Plot.plot({
y: {type: "sqrt"},
marks: [
Plot.raster(await vapor(), {
width: 360,
height: 180,
x: (d, i) => (i % 360) - 180 + 0.5,
y: (d, i) => 90 - ((i / 360) | 0) + 0.5,
fill: (d) => d,
interpolate: "nearest"
})
]
})Also note that if I try to use the interpolate option with a dense grid, it crashes because the x and y channels are missing. Plot.plot({
y: {type: "sqrt"},
marks: [
Plot.raster(await vapor(), {
width: 360,
height: 180,
fill: (d) => d,
interpolate: "nearest"
})
]
}) |
|
I think the challenge with “dense grid in abstract rather than projected coordinates” is that it doesn’t work as well for evaluating continuous functions. When you have a continuous function, we want to compute the grid in projected (scaled) coordinates and then invert it back into abstract (data) coordinates for the best results. Maybe we need to separate the code path for the continuous function case somehow; we could still provide shorthand for the dense grid case but not worry about optimizing it so much. |
|
Okay, I think this is looking really good now! There’s still some optimization that’s likely possible when you specify a dense grid of values, but only in the case where x and y are linear scales. If we figure out how to make that faster in the future that’s gravy but it shouldn’t change the API! From my side I’m ready to merge this and the parent branch into main and start working on documentation. |







The contour mark functions similarly to the raster mark, except that after generating the raster grid, it generates contour polygons using d3-contour (instead of an image). I figure we should flesh out this API to make sure it works well with the new raster mark in #1196.
TODO