Reproducible reporting with Quarto

A demonstration of the DataSphere Analytics report template

Théophile L. Mouton

2026-08-11

What this document is

This is a working demonstration of the reporting template I build for clients. Everything below is produced by a single .qmd file: the styling, the interactive figures, the map and the table. There is no manual assembly step, no copy-paste into Word, and no separate slide deck to keep in sync. Re-running the file with new data reproduces the entire document exactly as you see it.

The data used here are a small selection of large marine protected areas, with rounded areas and indicative centroids taken from public sources. They are here to exercise the template, not to make an analytical point.

An interactive figure

Hover any point for details, drag to zoom, double-click to reset. The line traces the cumulative area brought under protection by these designations.

Show code
plot_ly() |>
  add_trace(
    data = mpa,
    x = ~year, y = ~cumulative_km2 / 1e6,
    type = "scatter", mode = "lines",
    line = list(color = pal_ocean[["deep"]], width = 2, shape = "hv"),
    hoverinfo = "skip", name = "Cumulative area",
    showlegend = FALSE
  ) |>
  add_trace(
    data = mpa,
    x = ~year, y = ~cumulative_km2 / 1e6,
    type = "scatter", mode = "markers",
    color = ~basin, colors = basin_cols,
    marker = list(
      size = ~sqrt(area_km2) / 45 + 8,
      line = list(color = "white", width = 1.5),
      opacity = 0.9
    ),
    text = ~paste0(
      "<b>", name, "</b><br>",
      country, " · designated ", year, "<br>",
      format(area_km2, big.mark = " "), " km²"
    ),
    hoverinfo = "text"
  ) |>
  layout(
    xaxis = list(title = "Year of designation", gridcolor = "#eef3f6", zeroline = FALSE),
    yaxis = list(title = "Cumulative area (million km²)", gridcolor = "#eef3f6", zeroline = FALSE),
    legend = list(orientation = "h", y = -0.18, title = list(text = "")),
    hoverlabel = list(bgcolor = "white", bordercolor = pal_ocean[["surface"]]),
    font = list(family = "Source Sans 3, sans-serif", color = "#1a2332"),
    paper_bgcolor = "rgba(0,0,0,0)",
    plot_bgcolor = "rgba(0,0,0,0)",
    margin = list(t = 20)
  ) |>
  config(displayModeBar = FALSE)

Marker size scales with the area of each site. Colour separates ocean basins, and clicking a basin in the legend removes it from the plot.

A spatial view

The same sites on a Leaflet map. Circles are scaled by area, the layer control in the top right switches basemaps and filters basins, and each marker carries a popup.

Show code
mpa_pal <- colorFactor(unname(basin_cols), domain = names(basin_cols))

m <- leaflet(mpa, options = leafletOptions(worldCopyJump = FALSE)) |>
  addProviderTiles("Esri.WorldImagery", group = "Satellite") |>
  addProviderTiles("Esri.OceanBasemap", group = "Bathymetry") |>
  addProviderTiles("CartoDB.Positron", group = "Light") |>
  setView(lng = 10, lat = 0, zoom = 1.4)

for (b in names(basin_cols)) {
  d <- filter(mpa, basin == b)
  if (nrow(d) == 0) next
  m <- addCircleMarkers(
    m, data = d, lng = ~lon, lat = ~lat,
    radius = ~sqrt(area_km2) / 120 + 5,
    color = "white", weight = 1.5,
    fillColor = basin_cols[[b]], fillOpacity = 0.65,
    group = b,
    label = ~lapply(paste0(
      "<div style='font-family:Source Sans 3,sans-serif; line-height:1.45;'>",
      "<b style='color:", pal_ocean[["deep"]], ";'>", name, "</b><br>",
      "<span style='color:#445566;'>", country, " · ", year, "</span><br>",
      "<b>", format(area_km2, big.mark = " "), " km²</b></div>"
    ), htmltools::HTML),
    labelOptions = labelOptions(
      textsize = "13px",
      direction = "auto",
      sticky = FALSE,
      style = list(
        "background" = "white",
        "border" = paste0("1px solid ", pal_ocean[["surface"]]),
        "border-radius" = "4px",
        "box-shadow" = "0 2px 6px rgba(6,45,74,0.18)",
        "padding" = "6px 10px"
      )
    ),
    popup = ~paste0(
      "<div style='font-family:Source Sans 3,sans-serif;'>",
      "<b style='color:", pal_ocean[["deep"]], ";'>", name, "</b><br>",
      country, "<br>Designated ", year, "<br>",
      "<b>", format(area_km2, big.mark = " "), " km²</b></div>"
    )
  )
}

m |>
  addLayersControl(
    baseGroups = c("Satellite", "Bathymetry", "Light"),
    overlayGroups = names(basin_cols),
    options = layersControlOptions(collapsed = TRUE)
  ) |>
  addLegend(
    position = "bottomright", pal = mpa_pal, values = names(basin_cols),
    title = "Ocean basin", opacity = 0.8
  ) |>
  addScaleBar(position = "bottomleft")

Marker radius is illustrative rather than proportional to real footprint. In client work these become actual polygon boundaries, drawn from shapefiles or a spatial database.

A sortable table

The same records as a searchable table. Sorting, filtering and pagination all work in the browser, so a reader can interrogate the underlying numbers without asking for the data file.

Show code
reactable(
  mpa |> select(name, country, basin, year, area_km2),
  columns = list(
    name     = colDef(name = "Site", minWidth = 200),
    country  = colDef(name = "Country or territory", minWidth = 140),
    basin    = colDef(name = "Basin", maxWidth = 110),
    year     = colDef(name = "Designated", maxWidth = 110, align = "center"),
    area_km2 = colDef(
      name = "Area (km²)", maxWidth = 130, align = "right",
      format = colFormat(separators = TRUE),
      style = function(value) {
        w <- value / max(mpa$area_km2)
        list(background = sprintf(
          "linear-gradient(90deg, %s%s %s%%, transparent %s%%)",
          pal_ocean[["light"]], "55", w * 100, w * 100
        ))
      }
    )
  ),
  searchable = TRUE, defaultPageSize = 8, highlight = TRUE,
  striped = TRUE, compact = TRUE,
  defaultSorted = list(area_km2 = "desc"),
  theme = reactableTheme(
    borderColor = "#e3ecf2",
    headerStyle = list(
      background = pal_ocean[["foam"]],
      color = pal_ocean[["deep"]],
      fontWeight = 600
    )
  )
)

Why it is built this way

Three things matter to the clients I build these for. The document is reproducible, so a new data delivery means re-rendering rather than rebuilding. It is self-contained, a single HTML file that opens in any browser with no server, no login and no broken links a year later. And it is readable by non-specialists, because a decision-maker who can hover a point and read a popup will engage with results that a static PDF appendix would never get from them.

The template itself is CSS plus two HTML fragments referenced from the YAML header. Swapping the colour tokens and the footer produces a different institutional identity without touching any analysis code.

Note

Data shown are rounded public figures compiled for demonstration purposes only. Source: Protected Planet, UNEP-WCMC and IUCN.