# spatialwidget

#### David Cooley

#### 2025-11-29

# Spatialwidget

This package is designed to convert R data to JSON, ready for plotting on a map in an `htmlwidget`.

# Design

The basic idea of this package is to take an `sf` object or data.frame

    head( widget_capitals )
    #           country          capital        geometry
    #  1    Afghanistan            Kabul    69.11, 34.28
    #  2        Albania           Tirane    19.49, 41.18
    #  3        Algeria          Algiers     3.08, 36.42
    #  4 American Samoa        Pago Pago -170.43, -14.16
    #  5        Andorra Andorra la Vella     1.32, 42.31
    #  6         Angola           Luanda    13.15, -8.50

And convert it into pseudo-geojson ready to be parsed by javascript inside an htmlwidget

    js <- spatialwidget::widget_point(
      data = widget_capitals
      , fill_colour = "country"
      , legend = TRUE
      )

    substr( js$data, 1, 200 )
    #  [{"type":"Feature","properties":{"fill_colour":"#440154FF"},"geometry":{"geometry":{"type":"Point","coordinates":[69.11,34.28]}}},{"type":"Feature","properties":{"fill_colour":"#450356FF"},"geometry":

    substr( js$legend, 1, 100 )
    #  {"fill_colour":{"colour":["#440154FF","#450356FF","#450458FF","#45065AFF","#46085CFF","#460A5EFF","#

Notice the `fill_colour` column is now a hex colour, and the `geometry` column has been converted into `Point` coordinates.

This is basically it. The R object is now represented as JSON, having had a column of data changed into hex colours.

# R Interface

Here I describe the R functions available to you. However, these are deliberately limited in their capability, as this library is not intended to be used directly at the R-level. Instead, it’s designed to be integrated into packages at the C++ level, where you will call the C++ functions directly.

There are 4 R functions you can call for creating POINTs, LINEs, POLYGONs or origin-destination shapes. Each of these functions returns a list with two elements, `data` and `legend`.

- data : the R `data.frame` or `sf` object converted to pseudo-GeoJSON
- legend : a summary of the values and colours suitable for a legend on the map

## Pseudo-GeoJSON

The `data` is returned as pseudo-GeoJSON. Some plotting libraries can use more than one geometry, such as `mapdeck::add_arc()`, which uses an origin and destination. So spatialwidget needs to handle multiple geometries.

Typical GeoJSON will take the form

    [{"type":"Feature", "properties":{},"geometry":{"type":"Point","coordinates":[0,0]}}]

Whereas I’ve nested the geometries one-level deeper, so the pseudo-GeoJSON i’m using takes the form

    [{"type":"Feature", "properties":{},"geometry":{"myGeometry":{"type":"Point","coordinates":[0,0]}}}]

Where the `myGeometry` object is defined on a per-application bases. You are free to call this whatever you want inside your library, and have as many as you want.

## Examples

### Points

    l <- widget_point(
      widget_capitals[1:2, ]
      , fill_colour = "country"
      , legend = T
      )

    substr( l$data, 1, 200 )
    #  [{"type":"Feature","properties":{"fill_colour":"#440154FF"},"geometry":{"geometry":{"type":"Point","coordinates":[69.11,34.28]}}},{"type":"Feature","properties":{"fill_colour":"#FDE725FF"},"geometry":

### Lines

    l <- widget_line(
      widget_roads[1:2, ]
      , stroke_colour = "ROAD_NAME"
      , legend = T
      )

    substr( l$data, 1, 200 )
    #  [{"type":"Feature","properties":{"stroke_colour":"#440154FF","stroke_width":1.0},"geometry":{"geometry":{"type":"LineString","coordinates":[[145.014291,-37.830458],[145.014345,-37.830574],[145.01449,-

### Polygon

    l <- widget_polygon(
      widget_melbourne[1:2, ]
      , fill_colour = "AREASQKM16"
      , legend = F
      )

    substr( l$data, 1, 200 )
    #  [{"type":"Feature","properties":{"stroke_colour":"#440154FF","stroke_width":1.0,"fill_colour":"#440154FF"},"geometry":{"geometry":{"type":"Polygon","coordinates":[[[144.992523,-37.80249],[144.992645,-

# C++ API

The `spatialwidget::api::` namespace has 5 functions for converting your data into pseudo-geojson. Here are their definitions, the input data they expect and the type of output they produce.

### many-`sfc`-column `sf` to pseudo-geojson

    /*
     * sf object with one or many sfc columns
     *
     * expects `data` to be an sf object, where the geometry_columns is a string vector
     * containing the sfc colunm names (of sf) you want to use as the geometry objects
     * inside the GeoJSON
     */
    inline Rcpp::List create_geojson(
        Rcpp::DataFrame& data,
        Rcpp::List& params,
        Rcpp::List& lst_defaults,
        std::unordered_map< std::string, std::string >& layer_colours,
        Rcpp::StringVector& layer_legend,
        int& data_rows,
        Rcpp::StringVector& parameter_exclusions,
        Rcpp::StringVector& geometry_columns,
        bool jsonify_legend
      )

in - `sf` object with one or many `sfc` columns

out - geometries left as-is, returned in pseudo-geojson

### single-`sfc`-column `sf` to standard geojson

    /*
     * expects `data` to be an sf object, where the geometry_column is a string vector
     * of the sfc column names (of sf) you want to use as the geometry object inside the GeoJSON.
     *
     */
    inline Rcpp::List create_geojson(
        Rcpp::DataFrame& data,
        Rcpp::List& params,
        Rcpp::List& lst_defaults,
        std::unordered_map< std::string, std::string >& layer_colours,
        Rcpp::StringVector& layer_legend,
        int& data_rows,
        Rcpp::StringVector& parameter_exclusions,
        std::string& geometry_column,              // single geometry column from sf object
        bool jsonify_legend
    )

in - `sf` object with one `sfc` column

out - returns standard geojson

### `data.frame` with lon & lat columns to pseudo-geojson

    /*
     * expects `data` to be data.frame withn lon & lat columns. The geometry_columns
     * argument is a named list, list(myGeometry = c("lon","lat")), where 'myGeometry'
     * will be returned inside the 'geometry' object of the GeoJSON
     */
    inline Rcpp::List create_geojson(
        Rcpp::DataFrame& data,
        Rcpp::List& params,
        Rcpp::List& lst_defaults,
        std::unordered_map< std::string, std::string >& layer_colours,
        Rcpp::StringVector& layer_legend,
        int& data_rows,
        Rcpp::StringVector& parameter_exclusions,
        Rcpp::List& geometry_columns,
        bool jsonify_legend
    )

in - `data.frame` with lon & lat columns (each row is a POINT)

out - pseudo-geojson

### `data.frame` with lon, lat & elevation columns to pseudo-geojson

    /*
     * expects `data` to be data.frame withn lon & lat & elev columns. The 'bool elevation'
     * argument must be set to 'true', and the 'geometry_columns' should contain an 'elevation'
     * value - 'geometry_column <- list( geometry = c("lon","lat","elevation") )'
     */
    inline Rcpp::List create_geojson(
        Rcpp::DataFrame& data,
        Rcpp::List& params,
        Rcpp::List& lst_defaults,
        std::unordered_map< std::string, std::string >& layer_colours,
        Rcpp::StringVector& layer_legend,
        int& data_rows,
        Rcpp::StringVector& parameter_exclusions,
        Rcpp::List& geometry_columns,
        bool jsonify_legend,
        bool elevation
    )

in - `data.frame` with lon, lat and elevation columns (each row is a POINT)

out - pseudo-gejson

## C++ arguments

This set of arguments are commong to all the C++ functions

### Rcpp::DataFrame data

This will either be a data.frame with lon & lat columns, or an `sf` object.

### Rcpp::List params

A named list. The names are the arguments of the calling R function which will be supplied to the javascript widget. These are typically columns of `data`, or a single value that will be applied to all rows of `data`.

For example, an R function will look like

    add_layer <- function(
      data,
      fill_colour = NULL,
      stroke_colour = NULL,
      another_argument = TRUE
    )

And the list passed to c++ will be

    l <- list()
    l[["fill_colour"]] <- force( fill_colour )
    l[["stroke_colour"]] <- force( stroke_colour )

In this case, the `another_argument` is not passed to the javascript widget as part of the data, so we don’t include it in our list.

The javascript function inside a `htmlwidget` will then access the `stroke_colour` and `fill_colour` properties from the data.

This example code is taken from the javascript binding of `mapdeck::add_polygon()` to show you how I use it.

    const polygonLayer = new PolygonLayer({
        getLineColor: d => hexToRGBA2( d.properties.stroke_colour ),
        getFillColor: d => hexToRGBA2( d.properties.fill_colour ),
      });

### Rcpp::List lst_defaults

Either a named list, or an empty list.

You can use this list to supply default values to the widget.

    Rcpp::List scatterplot_defaults(int n) {
        return Rcpp::List::create(
            _["fill_colour"] = mapdeck::defaults::default_fill_colour(n)
        );
    }

    // use Either a named list,
    Rcpp::List lst_defaults = scatterplot_defaults( data_rows );  // initialise with defaults

    // or an empty object
    Rcpp::List lst_defaults;

### std::unordered_map< std::string, std::string > layer_colours

A c++ `unorderd_map` specifying colours and their associated opacity.

    std::unordered_map< std::string, std::string > polygon_colours = {
        { "fill_colour", "fill_opacity" },
        { "stroke_colour", "stroke_opacity"}
      };

These values will match the colour parameters used in the `params` list

    l <- list()
    l[["fill_colour"]] <- force( fill_colour )
    l[["stroke_colour"]] <- force( stroke_colour )

But you don’t have to supply the opacity, it will be set to ‘opaque’ by default.

### Rcpp::StringVector layer_legend

A vector of the colour values you want to use in a lenged.

    const Rcpp::StringVector polygon_legend = Rcpp::StringVector::create(
        "fill_colour", "stroke_colour"
      );

In this example, both `fill_colour` and `stroke_colour` will be returned in the legend data.

### int data_rows

The number of rows of `data`.

### Rcpp::StringVector parameter_exclusions

A vector describing the elements of `params` which will be excluded from the final JSON data.

    Rcpp::StringVector parameter_exclusions = Rcpp::StringVector::create("palette","legend","na_colour");

### bool jsonify_legend

A logical value indicating if you want the legend data returned as JSON (TRUE) or a a list (FALSE)

## Function-dependent arguments

### geometry_columns

Either an `Rcpp::List` or `Rcpp::StringVector`.

The `List` is used for `data.frame`s with lon & lat columns.

    df <- data.frame(lon = 0, lat = 0)
    geometry_column <- list( geometry = c("lon","lat") )

The `StringVector` is used for `sf` objects to specify the geometry columns.

    sf <- sf::st_sf( origin = sf::st_sfc( sf::st_point(c(0,0 ) ) ) )
    geometry_column <- c( "origin" )

### bool elevation

The `elevation` argument is used when the `data.frame` has a column of elevation data. When using the elevation you also need to supply this column in the `geometry_column` list.

    geometry_column <- list( geometry = c("lon","lat","elevation") )

## Example

Here’s an example implementation of the R, cpp and hpp files required to convert R data to pseudo-GeoJSON

widgetpoint.R

    #' Widget Point
    #'
    #' Converts an `sf` object with POINT geometriers into JSON for plotting in an htmlwidget
    #'
    #' @param data `sf` object with POINT geometries
    #' @param fill_colour string specifying column of `sf` to use for the fill colour
    #' @param legend logical indicating if legend data will be returned
    #' @param json_legend logical indicating if the lgend will be returned as JSON or a list
    #'
    #' @examples
    #'
    #' l <- widget_point( data = capitals, fill_colour = "country", legend = FALSE )
    #'
    #' @export
    widget_point <- function( data,
                              fill_colour,
                              legend = TRUE,
                              json_legend = TRUE ) {

      l <- list()
      l[["fill_colour"]] <- force( fill_colour )
      l[["legend"]] <- legend

      js_data <- rcpp_widget_point( data, l, c("geometry"), json_legend )

      return( js_data )
    }

widgetpoint.cpp

    #include <Rcpp.h>
    #include "spatialwidget/spatialwidget.hpp"
    #include "spatialwidget/spatialwidget_defaults.hpp"
    #include "spatialwidget/layers/widgetpoint.hpp"

    // [[Rcpp::export]]
    Rcpp::List rcpp_widget_point(
        Rcpp::DataFrame data,
        Rcpp::List params,
        Rcpp::StringVector geometry_columns,
        bool jsonify_legend ) {

      int data_rows = data.nrows();
      Rcpp::List defaults = point_defaults( data_rows );

      std::unordered_map< std::string, std::string > point_colours = spatialwidget::widgetpoint::point_colours;
      Rcpp::StringVector point_legend = spatialwidget::widgetpoint::point_legend;
      Rcpp::StringVector parameter_exclusions = Rcpp::StringVector::create("legend","legend_options","palette","na_colour");

      return spatialwidget::api::create_geojson(
        data,
        params,
        defaults,
        point_colours,
        point_legend,
        data_rows,
        parameter_exclusions,
        geometry_columns,
        jsonify_legend
      );
    }

/layers/widgetpoint.hpp

    #ifndef SPATIALWIDGET_WIDGETPOINT_H
    #define SPATIALWIDGET_WIDGETPOINT_H

    #include <Rcpp.h>
    namespace spatialwidget {
    namespace widgetpoint {

    // map between colour and opacity values
      std::unordered_map< std::string, std::string > point_colours = {
        { "fill_colour", "fill_opacity" }
      };

      // vector of possible legend components
      Rcpp::StringVector point_legend = Rcpp::StringVector::create(
        "fill_colour"
      );

    } // namespace widgetpoint
    } // namespace spatialwidget

    #endif

## Atomising geojson

As well as creating pseudo-GeoJSON, most of the functions also atomise the data.

When converting an `sf` object to GeoJSON it will typically create a FeatureCollection. ‘Atomising’ means it treats each row of the `sf` as it’s own Feature, and stores each one in a separate JSON object inside a JSON array (i.e., without combining them into a Feature Collection).

For example, we can create a GeoJSON FeatureCollection, convert it to `sf` and back again

    feat1 <- '{"type":"Feature","properties":{"id":1},"geometry":{"type":"Point","coordinates":[0,0]}}'
    feat2 <- '{"type":"Feature","properties":{"id":2},"geometry":{"type":"Point","coordinates":[1,1]}}'
    geojson <- paste0('[{"type":"FeatureCollection","features":[',feat1,',',feat2,']}]')
    sf <- geojsonsf::geojson_sf( geojson )
    sf
    #    id geometry
    #  1  1     0, 0
    #  2  2     1, 1

and going back the other way completes the round-trip and creates a FeatureCollection.

    geo <- geojsonsf::sf_geojson( sf )
    geo
    #  {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"id":1.0},"geometry":{"type":"Point","coordinates":[0.0,0.0]}},{"type":"Feature","properties":{"id":2.0},"geometry":{"type":"Point","coordinates":[1.0,1.0]}}]}

If we set it to ‘atomise’ when converting to geojson, an array of `Features` is returned

    geojsonsf::sf_geojson( sf, atomise = TRUE )
    #  {"type":"Feature","properties":{"id":1.0},"geometry":{"type":"Point","coordinates":[0.0,0.0]}}
    #  {"type":"Feature","properties":{"id":2.0},"geometry":{"type":"Point","coordinates":[1.0,1.0]}}

This structure is useful for sending to an htmlwidget because each object in the array can be parsed independently, without having to worry about iterating or parsing the entire Featurecollection.

Therefore, most of the GeoJSON functions inside spatialwidget will return the ‘atomised’ form.

# GeoJSON C++ API

You can by-pass the `spatialwidget::api::` namepsace and call the `spatialwidget::geojson::` api directly. However, doing so will only convert your data to pseudo-geojson, it won’t create colours or legends.

Here are the function definitions, the input data they expect and the type of output they produce.

### multi-`sfc`-column `sf` to atomised pseudo-geojson

      /*
      * a variation on the atomise function to return an array of atomised features
      */
      inline Rcpp::StringVector to_geojson_atomise(
          Rcpp::DataFrame& sf,
          Rcpp::StringVector& geometries ) {

    geojson <- spatialwidget:::rcpp_geojson_sf(sf = widget_arcs, geometries = c("origin","destination"))
    substr( geojson, 1, 500)
    #  [{"type":"Feature","properties":{"country_from":"Australia","capital_from":"Canberra","country_to":"Afghanistan","capital_to":"Kabul"},"geometry":{"origin":{"type":"Point","coordinates":[149.08,-35.15]},"destination":{"type":"Point","coordinates":[69.11,34.28]}}},{"type":"Feature","properties":{"country_from":"Australia","capital_from":"Canberra","country_to":"Albania","capital_to":"Tirane"},"geometry":{"origin":{"type":"Point","coordinates":[149.08,-35.15]},"destination":{"type":"Point","coordi

in - `sf` object with one or more `sfc` columns

out - atomised pseudo-geojson

### single-`sfc`-column `sf` to standard geojson

    inline Rcpp::StringVector to_geojson( Rcpp::DataFrame& sf, std::string geom_column )

    geojson <- spatialwidget:::rcpp_geojson( sf = widget_capitals, geometry = "geometry")
    substr( geojson, 1, 300)
    #  {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"country":"Afghanistan","capital":"Kabul"},"geometry":{"type":"Point","coordinates":[69.11,34.28]}},{"type":"Feature","properties":{"country":"Albania","capital":"Tirane"},"geometry":{"type":"Point","coordinates":[19.49,41.18]}}

in - `sf` object with one `sfc` column

out - standard GeoJSON

### `data.frame` with lon & lat columsn to atomised pseudo-geojson

      inline Rcpp::StringVector to_geojson_atomise(
          Rcpp::DataFrame& df,
          Rcpp::List& geometries ) // i.e., list(origin = c("start_lon", "start_lat", destination = c("end_lon", "end_lat")))
      {

    df <- sfheaders::sf_to_df( widget_capitals )

    geojson <- spatialwidget:::rcpp_geojson_df(df = df, list(geometry = c("x","y")) )
    substr( geojson, 1, 500 )
    #  [{"type":"Feature","properties":{"sfg_id":1,"point_id":1},"geometry":{"geometry":{"type":"Point","coordinates":[69.11,34.28]}}},{"type":"Feature","properties":{"sfg_id":2,"point_id":2},"geometry":{"geometry":{"type":"Point","coordinates":[19.49,41.18]}}},{"type":"Feature","properties":{"sfg_id":3,"point_id":3},"geometry":{"geometry":{"type":"Point","coordinates":[3.08,36.42]}}},{"type":"Feature","properties":{"sfg_id":4,"point_id":4},"geometry":{"geometry":{"type":"Point","coordinates":[-170.43,

in - `data.frame` with lon & lat columns

out - pseudo-GeoJSON atomised

### `data.frame` with lon, lat and elevation columns to atomised pseudo-geojson

      // list of geometries is designed for lon & lat columns of data
      inline Rcpp::StringVector to_geojson_z_atomise(
          Rcpp::DataFrame& df,
          Rcpp::List& geometries ) // i.e., list(origin = c("start_lon", "start_lat", destination = c("end_lon", "end_lat")))
      {

    df$z <- sample(1:500, size = nrow(df), replace = TRUE )
    geojson <- spatialwidget:::rcpp_geojson_dfz( df, geometries = list(geometry = c("x","y","z") ) )
    substr( geojson, 1, 500 )
    #  [{"type":"Feature","properties":{"sfg_id":1,"point_id":1},"geometry":{"geometry":{"type":"Point","coordinates":[69.11,34.28,327.0]}}},{"type":"Feature","properties":{"sfg_id":2,"point_id":2},"geometry":{"geometry":{"type":"Point","coordinates":[19.49,41.18,378.0]}}},{"type":"Feature","properties":{"sfg_id":3,"point_id":3},"geometry":{"geometry":{"type":"Point","coordinates":[3.08,36.42,372.0]}}},{"type":"Feature","properties":{"sfg_id":4,"point_id":4},"geometry":{"geometry":{"type":"Point","coor

in - `data.frame` with lon, lat and elevation columns

out - pseudo-GeoJSON atomised