Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
4975602
Add map coordinate system with projection shorthands
teunbrand May 6, 2026
b63bdb0
Merge branch 'main' into map_projections
teunbrand May 7, 2026
9b6487f
Add projection infrastructure to GeomTrait and CoordTrait
teunbrand May 7, 2026
7c192b7
Move spatial WKB serialization from stat to projection phase
teunbrand May 7, 2026
4aadf5d
pre_stat_tra
teunbrand May 7, 2026
f9b05c1
Refactor projection.rs into module directory
teunbrand May 7, 2026
5f1a8f8
Encapsulate projection constructors behind ::new()
teunbrand May 7, 2026
b3c5ba2
Move map_position_to_vegalite into ProjectionRenderer trait
teunbrand May 7, 2026
a4a4cce
Implement MapProjection renderer with identity projection
teunbrand May 8, 2026
66cc458
Implement horizon clipping for azimuthal map projections
teunbrand May 11, 2026
37ee888
Handle antimeridian crossing in hemisphere clip polygon
teunbrand May 11, 2026
cce8a1f
Store clip boundary in temp table for reuse across layers
teunbrand May 11, 2026
69c958d
Add computed field to Projection, remove CLIP_BOUNDARY_KEY const
teunbrand May 11, 2026
5c8bcb3
Render map panel background from projected clip boundary
teunbrand May 11, 2026
34636b2
Fix clip polygon wedge for pole+antimeridian projections
teunbrand May 11, 2026
81358c5
Frame map projection to data extent via scale/translate expressions
teunbrand May 12, 2026
036f55d
Add Inf/-Inf literal support to grammar and parser
teunbrand May 12, 2026
ddeda96
Add user-controllable bounds with Inf/null fallback, refactor map pro…
teunbrand May 12, 2026
530b4c1
update the CLAUDE.mds
teunbrand May 12, 2026
6290d83
Add graticule (lat/lon grid lines) to map projections
teunbrand May 12, 2026
c013a74
Introduce BBox struct in map projection for CRS-aware bounding boxes
teunbrand May 12, 2026
c2ad9ed
Deduplicate map projection helpers: grid_lines_wkt, query_scalar_string
teunbrand May 12, 2026
777c6e0
Reorganize map.rs into logical sections
teunbrand May 12, 2026
cc40f2d
Clean up writer map projection: extract helpers, merge impl blocks
teunbrand May 12, 2026
e4cd494
Merge branch 'main' into map_projections
teunbrand May 13, 2026
de2a632
Add visible area boundary for Mercator projection
teunbrand May 13, 2026
f855338
Add map projection shorthands: miller, equirectangular, stereographic…
teunbrand May 13, 2026
0d327ae
Add Interrupted Goode Homolosine projection support
teunbrand May 13, 2026
0e606fb
Add Robinson projection with densified rectangle boundary
teunbrand May 13, 2026
bd93110
Add fallback panel background for unsupported map projections
teunbrand May 13, 2026
ccc48ab
Add gnomonic and cylindrical equal-area projection shorthands
teunbrand May 14, 2026
db54b6a
Add pseudocylindrical projection shorthands: mollweide, sinusoidal, e…
teunbrand May 14, 2026
c17f8fd
unify rectangle specs
teunbrand May 14, 2026
ef6e35b
Add conical projection shorthands (albers, lambert_conformal) with se…
teunbrand May 15, 2026
0908787
Merge main into map_projections; fix geometry type mismatch in spatia…
teunbrand May 18, 2026
07d7077
Support non-EPSG:4326 source CRS in map projections
teunbrand May 18, 2026
e4d3c04
Infer Map coord from spatial layers and lon/lat aesthetics
teunbrand May 18, 2026
8d45138
Shift IGH interruption slits with lon_0
teunbrand May 18, 2026
2a92336
Project non-spatial point and text layers through the map CRS
teunbrand May 19, 2026
dd50678
Use longitude/latitude encoding channels for map projection layers
teunbrand May 19, 2026
3ba38c2
cargo fmt
teunbrand May 19, 2026
17f4c41
Extract map projection dispatch into MapSpecification trait
teunbrand May 20, 2026
c80f79a
Remove graticule seam splitting (handled by clip boundary slits)
teunbrand May 20, 2026
a7f6ffe
Refactor map.rs: decompose setup_clip_boundary, rename variables
teunbrand May 20, 2026
8e5f34b
Add center/parallel projection settings and consolidate MapSpecification
teunbrand May 21, 2026
1cdd9be
Honour clip => false setting in map projection
teunbrand May 21, 2026
95782d8
manual constraint on latitude center
teunbrand May 21, 2026
f13ba1d
annotations don't train bbox
teunbrand May 21, 2026
65637b4
add docs
teunbrand May 21, 2026
983671e
the pacification of clippy and cargo fmt
teunbrand May 21, 2026
bbd8c8c
rename center -> origin
teunbrand May 21, 2026
e750525
spelling
teunbrand May 21, 2026
f2991a7
add longlat
teunbrand May 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions doc/syntax/coord/map.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
---
title: Map
---

The map coordinate system facilitates the display of geographical data.
It can project data to various coordinate reference systems or projections.
It is used to display choropleths and other types of maps.

## Default aesthetics

* **Primary**: `lon` for longitude (horizontal)
* **Secondary**: `lat` for latitude (vertical)

Users can provide their own aesthetic names if needed.
For example, if using `x` and `y` aesthetics:

```ggsql
PROJECT x, y TO map
```

This maps `x` to longitude and `y` to latitude.
This is a convenience when converting from a Cartesian coordinate system, without having to modify all the mappings.

## Settings

* `clip`: Should data be removed if it appears outside the bounds of the coordinate system, or has become invalid due to the projection.
Defaults to `true`.
* `crs` A string giving the target Coordinate Reference System (CRS) holding parameters of a coordinate transformation.
This is only used with unnamed projections and therefore mutually exclusive with the `origin` and `parallel` settings.
By default, matches the Spatial Reference Identifiers (SRIDs) of geometry columns.
Typically takes one of the following forms:
* A [proj string](https://proj.org/en/stable/usage/quickstart.html) like `'+proj=longlat +lon_0=90'` for 2D longitude/latitude with 90° longitude offset.
* An [EPSG code](https://en.wikipedia.org/wiki/EPSG_Geodetic_Parameter_Dataset) like `'EPSG:3395'` for World Mercator projection.
* `source` A string giving the source CRS for the data to use as fallback when not explicit in geometry.
Useful when using non-spatial layers that don't carry SRIDs.
Defaults to `'EPSG:4326'` or longitude/latitude coordinates.
* `bounds` A numeric array of length 4 giving the bounding box of the area to view.
The numbers mean ('xmin', 'ymin', 'xmax', 'ymax') in that order, in the units of the `crs` argument.
Be mindful that your data might be in longitude/latitude degrees, but the `crs` might use meters, or vice versa.
Bounds can have `Inf`/`-Inf` values to indicate the visible area's extent.
Using `null` values indicates to fall back to the data's extent, so only partial bounds need to be indicated.
* `origin` Projection origin location.
This is only used with named projections and therefore mutually exclusive with `crs`.
One of the following:
* A scalar number, setting the origin longitude or 'central meridian' in degrees between −180 and +180.
Corresponds to the `+lon_0` proj string parameter.
* An array of two numbers, where the first number is the scalar option above.
The second number sets the latitude of origin in degrees between −90 and +90.
The latter corresponds to the `+lat_0` proj string parameter.
If unsupported by the projection, the latitude is silently ignored.
* `parallel` Setting for conic or cylindrical projections.
This is only used with named projections and therefore mutually exclusive with `crs`.
If unsupported by the projection, this setting is silently ignored.
One of the following:
* A scalar number, setting the standard parallel as degrees between −90 and +90.
Corresponds to the `+lat_1` proj string parameter.
* An array of two numbers, where the first number is the scalar option above.
The second number sets the second standard parallel as degrees between −90 and +90.

## Supported projections

There are two main ways to set the map projection.
One is via a named projection, which allows for the `origin` and `parallel` settings.

```ggsql
PROJECT TO orthographic SETTING origin => (150, 52)
```

The second option is to use the generic `PROJECT TO map` together with the `crs` setting.
The code below is equivalent to the code above.

```ggsql
PROJECT TO map SETTING crs => '+proj=ortho +lon_0=150 +lat_0=52'
```

In this context, 'supported' means that we are reasonably confident that we can draw a serviceable map.
Below follows a table giving an overview of the supported projections.
The projections are recognised by name in the 'string' column or from the 'proj string' CRS setting.
More information about them can be found on the [PRØJ website](https://proj.org/en/stable/operations/projections/index.html).

| name | string | origin | parallel | proj string |
|---------------------------------------|-------------------------|--------|--------------|------------------|
| Geographic (unprojected) | `geographic` | 0 | NA | `+proj=longlat` |
| Mercator | `mercator` | 0 | NA | `+proj=merc` |
| Orthographic | `orthographic` | (0, 0) | NA | `+proj=ortho` |
| Miller Cylindrical | `miller` | 0 | NA | `+proj=mill` |
| Equidistant Cylindrical (Plate Carée) | `equirectangular` | 0 | NA | `+proj=eqc` |
| Stereographic | `stereographic` | (0, 0) | NA | `+proj=stere` |
| Gnomonic | `gnomonic` | (0, 0) | NA | `+proj=gnom` |
| Cylindrical Equal Area | `equal_area` | 0 | NA | `+proj=cea` |
| Mollweide | `mollweide` | 0 | NA | `+proj=moll` |
| Sanson-Flamsteed Sinusoidal | `sinusoidal` | 0 | NA | `+proj=sinu` |
| Eckert IV | `eckert4` | 0 | NA | `+proj=eck4` |
| Natural Earth | `natural` | 0 | NA | `+proj=natearth` |
| Winkel Tripel | `winkel_tripel` | 0 | NA | `+proj=wintri` |
| Albers Equal Area | `albers` | (0, 0) | (29.5, 45.5) | `+proj=aea` |
| Lambert Conformal Conic | `lambert_conformal` | (0, 0) | (29.5, 45.5) | `+proj=lcc` |
| Lambert Azimuthal | `lambert` | (0, 0) | NA | `+proj=laea` |
| Azimuthal Equidistant | `azimuthal_equidistant` | (0, 0) | NA | `+proj=aeqd` |
| Interrupted Goode Homolosine | `igh` | 0 | NA | `+proj=igh` |
| Robinson | `robinson` | 0 | NA | `+proj=robin` |

The way to draw an unsupported map is to use the `crs` setting.
Unsupported maps are likely drawn without projection-appropriate clipping.

## Examples

Note that depending on your reader, you may need to activate modules for spatial analysis.

```{ggsql}
-- For example, for DuckDB, one could use:
INSTALL spatial;
LOAD spatial;
```

### Via named projection

```{ggsql}
VISUALISE continent AS fill FROM ggsql:world
DRAW spatial
PROJECT TO robinson
```

### Via proj string

```{ggsql}
VISUALISE continent AS fill FROM ggsql:world
DRAW spatial
PROJECT TO map SETTING crs => '+proj=robin'
```

### Setting the origin

```{ggsql}
VISUALISE continent AS fill FROM ggsql:world
DRAW spatial
PROJECT TO orthographic
SETTING origin => (133.77, -25.27)
```

### Zooming in

The default zoom behaviour is to zoom in where the data is.

```{ggsql}
VISUALISE continent AS fill FROM ggsql:world
DRAW spatial
FILTER subregion == 'Western Africa'
PROJECT TO orthographic
```

An alternative is to zoom using the `bounds` setting.
Note that while `ggsql:world` is defined in degrees longitude/latitude, the `orthographic` projection uses metres as unit.

```{ggsql}
VISUALISE continent AS fill FROM ggsql:world
DRAW spatial
PROJECT TO orthographic
SETTING bounds => (-1868152, 468481, 1638882, 2917218)
```

The `bounds` arguments can take infinites to take the visible area's extreme values, or `null` to take the data's bounds.

```{ggsql}
VISUALISE continent AS fill FROM ggsql:world
DRAW spatial
FILTER subregion == 'Western Africa'
PROJECT TO orthographic
SETTING bounds => (-1868152, -Inf, 1638882, null)
```

### Parallels

Default conic projections aren't always kind to the southern hemisphere.
This can be changed by tweaking the `parallel` setting.

```{ggsql}
VISUALISE continent AS fill FROM ggsql:world
DRAW spatial
PROJECT TO albers SETTING parallel => (-29.5, -45.5)
```
2 changes: 1 addition & 1 deletion doc/syntax/coord/polar.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Users can provide their own aesthetic names if needed. For example, if using `x`
PROJECT y, x TO polar
```

This maps `y` to radius and `x` to angle. This is useful when converting from a cartesian coordinate system without editing all the mappings.
This maps `y` to radius and `x` to angle. This is useful when converting from a Cartesian coordinate system without editing all the mappings.

## Settings
* `clip`: Should data be removed if it appears outside the bounds of the coordinate system. Defaults to `true`
Expand Down
12 changes: 4 additions & 8 deletions src/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,10 @@ Grammar lives in [`/tree-sitter-ggsql/`](../tree-sitter-ggsql/) — when adding
| `duckdb.rs` | DuckDB (in-memory or file) | `duckdb` (default) |
| `sqlite.rs` | SQLite | `sqlite` (default) |
| `odbc.rs` | ODBC | `odbc` (default) |
| `snowflake.rs` | Snowflake (very small placeholder today) | — |
| `connection.rs` | Connection-string parsing for all of the above | — |
| `data.rs`, `spec.rs` | `Spec` type returned by `execute()`, plus DataFrame conversion | — |

`SqlDialect` trait in `mod.rs` lets each driver supply its own type names and information-schema queries. PostgreSQL has a feature flag (`postgres`) but no driver file yet.
`SqlDialect` trait in `mod.rs` lets each driver supply its own type names, information-schema queries, and spatial helper methods (`sql_st_transform`, `sql_geometry_to_wkb`, `sql_geometry_bbox`, `sql_spatial_setup`).

### `execute/`

Expand Down Expand Up @@ -97,15 +96,12 @@ Defined in `Cargo.toml`:
| `sqlite` | ✓ | SQLite reader |
| `odbc` | ✓ | ODBC reader |
| `parquet` | ✓ | Parquet support in readers/data |
| `ipc` | ✓ | Arrow IPC support |
| `spatial` | ✓ | Spatial/geometry support (geozero for WKT↔GeoJSON) |
| `vegalite` | ✓ | Vega-Lite writer |
| `builtin-data` | ✓ | Bundled penguins/airquality datasets |
| `postgres` | — | PostgreSQL reader (declared, not yet implemented) |
| `ggplot2` | — | ggplot2 writer (declared, not yet implemented) |
| `all-readers` | — | `duckdb` + `postgres` + `sqlite` + `odbc` |
| `all-writers` | — | `vegalite` + `ggplot2` + `plotters` |
| `all-readers` | — | `duckdb` + `sqlite` + `odbc` |

`ggsql-wasm` builds with `default-features = false` plus `vegalite`, `sqlite`, `builtin-data`. `ggsql-jupyter` builds with `duckdb`, `sqlite`, `odbc`, `vegalite`.
`ggsql-wasm` builds with `default-features = false` plus `vegalite`, `sqlite`, `builtin-data`. `ggsql-jupyter` builds with `duckdb`, `vegalite`.

## Testing

Expand Down
2 changes: 1 addition & 1 deletion src/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ bytes = { workspace = true }
adbc_core = { version = "0.23", optional = true }

# Spatial
geozero = { workspace = true, optional = true, features = ["with-wkb", "with-geojson"] }
geozero = { workspace = true, optional = true, features = ["with-wkb", "with-wkt", "with-geojson"] }

# Serialization
serde.workspace = true
Expand Down
13 changes: 13 additions & 0 deletions src/execute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,19 @@ pub fn prepare_data_with_reader(query: &str, reader: &dyn Reader) -> Result<Prep
layer_queries.push(layer_query);
}

// Apply projection transforms (post-stat, pre-fetch)
let mut project = specs[0]
.project
.take()
.unwrap_or_else(crate::plot::projection::Projection::cartesian);
project.apply_projection_transforms(
&specs[0].layers,
&mut layer_queries,
dialect,
&execute_query,
)?;
specs[0].project = Some(project);

// Phase 2: Deduplicate and execute unique queries
let mut query_to_result: HashMap<String, DataFrame> = HashMap::new();
for (idx, q) in layer_queries.iter().enumerate() {
Expand Down
2 changes: 2 additions & 0 deletions src/execute/scale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1723,6 +1723,8 @@ mod tests {
coord,
aesthetics,
properties: std::collections::HashMap::new(),
map_projection: None,
computed: std::collections::HashMap::new(),
});

// Create scale for pos2 (theta in polar) without explicit expand
Expand Down
Loading
Loading