Layer densification/munching for map projections#472
Conversation
Extend the signature so geoms that need group-aware densification (line, path, polygon) will have access to the layer's grouping columns at projection time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a map projection requires CRS transformation, these geoms now subdivide long edges before projecting. This produces smooth curves in projected space instead of straight-line segments between vertices. Adds `needs_projection()` to centralize the guard logic shared by all three geoms. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap the entire interpolation delta in COALESCE so that when the next vertex is NULL (last point in an open line/path), the expression collapses to the original position instead of propagating NULL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Continuous tiles (pos1min/pos1max/pos2min/pos2max) are expanded to polygon vertices, densified, and projected so rectangle edges curve correctly on map projections. The tile geom mutates its mappings after polygonization to reflect the new pos1/pos2 column structure, keeping the logic self-contained and eliminating redundant SQL wrappers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The resolved clip state is already available in projection.properties — spatial now reads it directly from there instead of receiving it as a separate argument. map.rs writes the resolved value back after determining whether the boundary materialized. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rules with clipping expand into densified line segments: each rule is cross-joined to its clip-boundary extent, densified along the spanning axis, clipped to the boundary, then projected. The writer renders the result as a line mark ordered by row index. The apply_projection trait method now accepts a mutable parameters map so Rule can signal the orientation override downstream. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All densifiable geoms (tile, segment, ribbon, rule) and the writer now reference a single constant instead of per-geom magic strings. The SQL column is uniformly __ggsql_densify_id__. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eter Instead of detecting densification state by checking whether partition_by contains the densify ID column name, each geom now sets a typed parameters["densified"] = true during apply_projection. The writer and validate_aesthetics check this parameter directly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
thomasp85
left a comment
There was a problem hiding this comment.
This generally looks fine, but I low-key hate how much code and logic this requires...
Did you toy with converting the positions to a spatial column instead and let the db's spatial extension handle it? Or is densification not build into these extensions..?
| fn apply_projection( | ||
| &self, | ||
| query: &str, | ||
| projection: &Projection, | ||
| dialect: &dyn SqlDialect, | ||
| mappings: &mut Mappings, | ||
| partition_by: &mut Vec<String>, | ||
| parameters: &mut std::collections::HashMap<String, crate::plot::types::ParameterValue>, | ||
| ) -> Result<String> { | ||
| if !needs_projection(projection) { | ||
| return Ok(query.to_string()); | ||
| } | ||
|
|
||
| let columns = mappings.column_names(); | ||
| let (expanded, expanded_columns) = expand_ribbon_to_polygon(query, &columns, partition_by); | ||
|
|
||
| partition_by.push(naming::DENSIFY_ID_COLUMN.to_string()); | ||
| parameters.insert("densified".to_string(), ParameterValue::Boolean(true)); | ||
|
|
||
| let densified = densify_edges( | ||
| &expanded, | ||
| dialect, | ||
| &expanded_columns, | ||
| partition_by, | ||
| Some("__ggsql_vertex__"), | ||
| true, | ||
| 1.0, | ||
| 360, | ||
| ); | ||
| let projected = | ||
| project_position_columns(&densified, projection, dialect, &expanded_columns)?; | ||
|
|
||
| mappings.insert_column("pos2", "pos2"); | ||
| mappings.insert_column("pos2min", "pos2"); | ||
| mappings.insert_column("pos2max", "pos2"); | ||
|
|
||
| Ok(projected) | ||
| } |
There was a problem hiding this comment.
I think I have a hard time figuring out how a ribbon would look like in combination with a spatial projection but I guess it doesn't hurt
There was a problem hiding this comment.
Yeah my initial thought was that it could show things like tornado tracks, where the size increases with wind speeds. However, as this is anchored to horizontal/vertical orientation that doesn't make much sense for arbitrary tornado paths. But at the point I realised this, it was already working 🤷
Yeah doing geometry like interpolation and polygonisation in SQL sparks joy in zero people.
The main barrier for this is that only PostGIS and not DuckDB/SpatiaLite implement densification IIRC. |
Decouple expand_rule_to_segment from clip_boundary_table by accepting a bbox_expr argument. Gate spatial steps (bbox source, ST_Contains clip) behind CoordKind::Map match arms so future non-map projections can reuse the generic expand→densify→project pipeline. Also include the projection name in the unsupported-geom error message. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This PR aims to fix #163.
Briefly, from discussion it appeared we didn't need to implement generic densification, but would still need it for map projections.
This PR enables densification for line, path, polygon, tile, segment, ribbon and rule. Earlier we already had spatial, text and point. We do not implement densification for bar, histogram, boxplot, violin, density, smooth, area or range.
The general pattern is this:
apply_projection()checks if we have a map projectionexpand_{layer}_to_polygon()for layers that need custom logicdensify_edges()for interpolationpartition_bycolumns and tracking the densified state viaparameters["densified"].parameters["densified"]and writes a polygon/line instead of rect/ribbon/rule/segment etc.The odd one out is the rule layer, because it needs the bounding box for knowing where to draw the line, so it has a bit more spatial specific logic than the rest of em.