Skip to content

contour mark#1214

Merged
mbostock merged 30 commits intombostock/image-datafrom
mbostock/contour
Jan 10, 2023
Merged

contour mark#1214
mbostock merged 30 commits intombostock/image-datafrom
mbostock/contour

Conversation

@mbostock
Copy link
Copy Markdown
Member

@mbostock mbostock commented Jan 8, 2023

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

  • fill as a function
  • dense grids (volcano)
  • non-gridded samples (ca55)

@mbostock mbostock requested a review from Fil January 8, 2023 02:02
@mbostock
Copy link
Copy Markdown
Member Author

mbostock commented Jan 8, 2023

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!

@mbostock mbostock marked this pull request as ready for review January 8, 2023 05:46
Comment thread test/plots/function-contour.js Outdated
},
marks: [
Plot.contour({
value: (x, y) => x * y * Math.sin(x) * Math.cos(y),
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?). 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes it would be very nice; let's just throw an error if both are functions?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will work on more shorthand tomorrow.

Comment thread src/marks/raster.js
@Fil
Copy link
Copy Markdown
Contributor

Fil commented Jan 8, 2023

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:

  • throw an error or give a warning to say "the samples don't cover the whole raster, please select an interpolate strategy (e.g.: none, or barycentric)"
  • default to barycentric+blur3

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).
@Fil
Copy link
Copy Markdown
Contributor

Fil commented Jan 8, 2023

I think blurring + random-walk is also a good solution to #1196 (comment) (see dd06ea05c commit log).
rasterPenguinsBlur

mbostock and others added 4 commits January 8, 2023 11:00
…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…
@Fil
Copy link
Copy Markdown
Contributor

Fil commented Jan 9, 2023

  • Fixed two bugs I found while playing with the contours and rasters
  • Checked degenerate cases such as n=1 or n=2 points.

About the shorthand and defaults, I suggest we follow a similar convention as in Plot.density:

  • default to black stroke, no fill; use fill: value if we want a colored fill, same for stroke (for the density mark, the special keyword is "density"; here we can use "value" or "threshold").
  • the value of a point is specified with the value option, as a channel if we have data, and as a function f(x,y) if we accept a function sampler.

@Fil
Copy link
Copy Markdown
Contributor

Fil commented Jan 9, 2023

For the record here's what the vapor-barycentric looked like :)
vapor-bary-bug

Comment thread src/marks/contour.js Outdated
@mbostock mbostock requested a review from Fil January 9, 2023 18:19
@Fil
Copy link
Copy Markdown
Contributor

Fil commented Jan 9, 2023

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.

@Fil
Copy link
Copy Markdown
Contributor

Fil commented Jan 9, 2023

Ah no! It works—it's just that the defaults have changed 👍

@mbostock
Copy link
Copy Markdown
Member Author

mbostock commented Jan 9, 2023

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:

Screenshot 2023-01-09 at 11 54 21 AM

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:

Screenshot 2023-01-09 at 11 55 27 AM

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.

Screenshot 2023-01-09 at 11 58 09 AM

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.

Screenshot 2023-01-09 at 11 57 49 AM

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.

Screenshot 2023-01-09 at 11 59 06 AM

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).

Screenshot 2023-01-09 at 12 00 40 PM

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.

TypeError: Cannot read properties of undefined (reading '362')
Plot.plot({
  y: {type: "sqrt"},
  marks: [
    Plot.raster(await vapor(), {
      width: 360,
      height: 180,
      fill: (d) => d,
      interpolate: "nearest"
    })
  ]
})

@mbostock
Copy link
Copy Markdown
Member Author

mbostock commented Jan 9, 2023

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.

@mbostock
Copy link
Copy Markdown
Member Author

mbostock commented Jan 9, 2023

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.

Copy link
Copy Markdown
Contributor

@Fil Fil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌐 🎉 👍 🚢

@mbostock mbostock merged commit 4bb1716 into mbostock/image-data Jan 10, 2023
@mbostock mbostock deleted the mbostock/contour branch January 10, 2023 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants