observablehq / plot

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

waffle mark 🧇 #2040

Closed mbostock closed 1 month ago

mbostock commented 5 months ago

Fixes #2029.

mbostock commented 1 month ago

Current status:

Screenshot 2024-08-01 at 11 43 54 AM

I’m now computing the number of waffle cell columns correctly, and rendering them as a pattern. But I need to replace the rect elements generated by BarY with a path element so that the top & bottom cells aren’t truncated (unless there’s a fractional value).

mbostock commented 1 month ago

Now with fractional cell rendering!

Screenshot 2024-08-01 at 1 15 25 PM
mbostock commented 1 month ago

Whew! 😅 That was a lot of work but I’m pretty happy with how this turned out.

mbostock commented 1 month ago

localhost_5173_plot_marks_waffle

Fil commented 1 month ago

I was wondering how to use this for ISOTYPE charts, and it appears that it's possible to use the render transform to edit the patterns’ contents:

render: (index, scales, values, dimensions, context, next) => {
  const g = next(index, scales, values, dimensions, context);
  const pattern = d3.select(g).selectAll("pattern");
  pattern.selectAll("rect").remove();
  pattern.append("text").attr("x", 10).attr("y", 15).text("🚀");
  return g;
}
Capture d’écran 2024-08-04 à 12 18 39

This gives two ideas for future enhancements:

The rendered pattern might also be easier to manipulate if it was centered on the origin with a viewBox rather than extending from it (in that case, the values for x and y above would essentially be zero).

@@ -62,10 +62,11 @@ function waffleRender(y) {
     const basePattern = document.createElementNS(namespaces.svg, "pattern");
     basePattern.setAttribute("width", cellsize);
     basePattern.setAttribute("height", cellsize);
+    basePattern.setAttribute("viewBox", [-cellsize / 2, -cellsize / 2, cellsize, cellsize]);
     basePattern.setAttribute("patternUnits", "userSpaceOnUse");
     const basePatternRect = basePattern.appendChild(document.createElementNS(namespaces.svg, "rect"));
-    basePatternRect.setAttribute("x", gap / 2);
-    basePatternRect.setAttribute("y", gap / 2);
+    basePatternRect.setAttribute("x", (gap - cellsize) / 2);
+    basePatternRect.setAttribute("y", (gap - cellsize) / 2);
     basePatternRect.setAttribute("width", cellsize - gap);
     basePatternRect.setAttribute("height", cellsize - gap);
     if (rx != null) basePatternRect.setAttribute("rx", rx);
Fil commented 1 month ago

It makes total sense that the number of rows/columns is completely automatic and not configurable, but it might be useful to call this out explicitly in the documentation? And give hints on how to alter the number of rows and columns (such as layering several marks like in the Syrian teenagers example, changing the chart's aspect ratio, using scale insets, etc).

Fil commented 1 month ago

It might be nice in the future to allow a configurable orientation (e.g. filling rows from the left instead of from the right). Not a blocker at all, I'm not even sure I would use it.

mbostock commented 1 month ago

It makes total sense that the number of rows/columns is completely automatic and not configurable, but it might be useful to call this out explicitly in the documentation?

That’s already called out here:

The waffle mark automatically determines the appropriate number of cells per row or per column (depending on orientation) such that the cells are square, don’t overlap, and are consistent with position scales.

It would also be very easy to expose this as an option. In fact I had it as an option in the past. But it was a footgun: if you hard-code the value it’s easy to produce overlapping waffles. In practice, what I want is to guarantee that the rows/columns is either a factor or a multiple of the corresponding position scale. The waffle mark doesn’t have that information easily available at render time, though (since the axis could be rendered after the waffle, or there could be multiple axes)… maybe an option to limit it to a power of ten times 1, 2, 5? Anyway.

The number of rows/columns is also heavily affected by the values, so I’m not sure I have much useful hints. Maybe just, if you have narrower bands, you have fewer cells per row/column? Setting padding on the ordinal scale for example.

mbostock commented 1 month ago

It might be nice in the future to allow a configurable orientation (e.g. filling rows from the left instead of from the right).

Sure… though I’m skeptical of adding such an obscure feature unless someone can explain why it’s worth adding. You could always do it with a custom mark or render transform.

mbostock commented 1 month ago

The ISOTYPE chart idea is interesting, but I’m not sure how well the waffle would work for that since often the glyphs are non-square and can vary in size across series. There’s no meaningful position encoding in an ISOTYPE chart; you’re just responsible for counting glyphs to perceive the encoded value.

image

But for sure there are more extensions we could do here than square/rounded squares for the pattern! I don’t think we should implement these now, with the possible exception of non-square aspect ratios as previously mentioned.