observablehq / plot

A concise API for exploratory data visualization implementing a layered grammar of graphics
https://observablehq.com/plot/
ISC License
4.44k stars 179 forks source link

expose the projection to make it reusable #1191

Open Fil opened 1 year ago

Fil commented 1 year ago

maybe just as plot.scale("projection")? If the goal is to make it reusable, it doesn't matter if it's an opaque {stream}, and the code is ultra-simple. If we extend the scope and want to get the name back, it's much more difficult.

jheer commented 8 months ago

Exposing projection information would be very useful to Mosaic vgplot. We could then support brushing/filtering over explicitly projected data, at least initially for planar projections and those with separable lon -> x, lat -> y mappings (like unrotated mercator or equirectangular).

I have a working prototype of this locally, which extends Plot's projection.js like so:

return {
  width: dx,
  height: dy,
  offset: [marginLeft + insetLeft, marginTop + insetTop],
  translate: [tx, ty],
  scale: k,
  invert: projection.invert, // may be undefined
  stream: (s) => projection.stream(transform.stream(clip(s)))
};

One also needs to first define let k = 1 in the outer scope. I'm exporting dimension information (width, height, offset) to infer the equivalent of scale ranges and to aid setting the boundaries of the brushable region. I use the (translate, scale) data to create my own invert methods (including optimized 1D variants for planar projections). Alternatively, an end-to-end invert method that takes any affine transform into account would work fine.

The only other change is to modify plot.js to expose the projection:

figure.projection = () => context.projection;

I assume you would want to run this through an exposeProjection method that makes a defensive copy.

Not sure how well something like the above would fit in with your plans and sensibilities, but I can make a PR if it does.

Fil commented 8 months ago

Not to say that we should absolutely support every funky projection out there, but I'm weary of an approach that by design wouldn't allow to work, for example, with polar projections, or maps rotated to include the antimeridian?

Have you thought of going in the opposite direction? If mosaic had only access to the stream — or maybe a convenience method projection(lon, lat) => [x, y] returned by Plot, could it initialize its brushing component by computing all the screen positions (once); these derived dimensions would be added to the datacube, and brushing would work normally. This would make no assumption on what the projection returns.

The drawback of this approach —and I guess that's what you're trying to avoid?— is that you have to compute projected values [x,y] for the whole dataset. But since brushing does not require an extreme precision (that is, a precision of 1 pixel or 0.5 pixel is enough), there might be ways to optimize this even when you have a bazillion data points, by binning or sampling?

Happy to explore this with you in any case—the more maps we can make, the 👍

jheer commented 8 months ago

Yes, there are plenty of non-invertible projections that would be nice to support. As you note, we can compute the projection as a pre-processing step, then rely on the projected (x, y) values. We do this in-database for examples like the Gaia star map and NYC taxis. In the Mosaic context, we need these projected coordinates to reside in-database for scalable and interoperable filtering.

However, all that resides outside of Plot. So for cases where we want to use Plot projections (and their nice defaults, scaling, etc) we currently have a bit of a chicken-and-egg problem. So I've been exploring cases where we can create meaningful selections over lon/lat via projection inversion. If ultimately this proves to be a dead-end, so be it!

In any case it would be nice to be able to access the projection - even if just the stream!

jaanli commented 7 months ago

+1 on this - currently struggling to figure out how to use the Imago projection from https://observablehq.com/d/51f92faafc3fb631 before realizing it is not supported: https://observablehq.com/@observablehq/plot-projections?collection=@observablehq/plot

(modifying this: https://github.com/jaanli/jaan.li/blob/dd7c3ef57bfb4dc607ec01f60ba0d5abf7ada5b9/docs/components/narrativeMap.js from the electricity example: https://github.com/observablehq/framework/blob/main/examples/eia/docs/components/map.js)

Fil commented 7 months ago

@jaanli I can show you how to use the Imago projection, but let's please open a separate discussion; this issue is about the reverse operation where you want to read the projection from a chart.

jaanli commented 7 months ago

Will do! My bad @Fil I had thought that the current operations involve filtering projected data at https://jaan.li -- sorry if I misunderstood - will open a separate discussion :)

jaanli commented 7 months ago

Would also want to use this to enable filtering for data like this: https://jaanli.github.io/new-york-real-estate between Mosaic, Plot, and Protomaps / Maplibre GL types of visualizations like this...