Writes the indicator layers of a store built by build_obis_h3_duckdb() to
a directory of hive-partitioned Parquet files that a static web app can read
directly with DuckDB-WASM (one small file per map layer and H3 resolution):
Usage
obis_h3_export_parquet(
con,
dir_out,
release,
res_decade = 1:5,
res_year = 1:5,
years = NULL,
res_species = c(3L, 4L, 5L),
species_decades = TRUE,
res_species_decade = c(3L, 4L),
res_species_flat = 1:3,
species_flat_split = c(`3` = 1L),
split_row_group_rows = 2048L,
heads = TRUE,
res_heads = 1:7,
parent_res = c(`6` = 2L, `7` = 3L),
parent_res_taxon = c(`6` = 0L, `7` = 0L),
parent_min_rows = 50000,
max_open = EXPORT_MAX_OPEN,
compression = "zstd",
row_group_rows = 10000L,
overwrite = FALSE,
decades = seq(1960L, 2020L, by = 10L),
esn = 50L,
place_collections = NULL,
place_exact = NULL,
place_exact_occ = NULL,
temp_dir = NULL,
max_temp_dir_size = NULL,
memory_limit = NULL,
threads = NULL,
verbose = TRUE
)Arguments
- con
connection from
obis_store_connect()(read-only is fine).- dir_out
output directory; files go under
dir_out/release/.- release
release tag, e.g.
"v20260728"(the date of the OBIS sync the store was built from); recorded inrelease.json. Itsobis_snapshotis the environment variableOBIS_DATA_DATE(the newest OBIS object's date, whichrefresh_obis_h3.shpasses) when that is aYYYY-MM-DDdate, else the date in the tag.- res_decade
H3 resolutions for the decade layers (default 1-5).
- res_year
H3 resolutions for the year layers (default 1-5);
integer(0)skips them.- years
years to export in the year layers; default NULL, every year present.
- res_species
H3 resolutions of the species table (default 3, 4, 5);
integer(0)skips it.- species_decades
also write the per-decade species table
species_decade/(default TRUE, 0.11.0), atres_species_decade.- res_species_decade
the resolutions of the per-decade species table (default 3 and 4: the decade filter on a WoRMS subtree).
- res_species_flat
the resolutions of the unpartitioned species table
species_flat/res=<r>.parquet(default 1 to 3, 0.12.0: the cell query; the finest from the store, the others rolled up from it);integer(0)skips it.- species_flat_split
resolution = parent resolution of the split
species_flat/res=<r>/p=<parent>.parquet(defaultc("3" = 1L)); NULL writes none. A resolution not inres_species_flatis skipped.- split_row_group_rows
rows per row group of the split files (default 2048).
- heads
write the pre-aggregated heads as taxon layers (default TRUE; needs the species table and the store's
taxontable).- res_heads
the resolutions of the heads (default 1-7; res 6 and 7 are split by
parent_res_taxonlike the rank layers).- parent_res
named integer vector, fine resolution -> parent resolution, for the
allandeovlayers: those resolutions are written one file per parent cell (res=<r>/p=<parent>/). Defaultc("6" = 2L, "7" = 3L);NULLwrites every resolution whole.- parent_res_taxon
the same for the taxon layer (default
c("6" = 0L, "7" = 0L), the base cell).- parent_min_rows
a (layer key, res) with at most this many rows is written whole even when its res is in
parent_res(default 50000).- max_open
no longer used (0.11.0): every file is written by its own
COPY, so no more than one is open at a time. Kept so old calls still run.- compression
Parquet compression codec (default
"zstd").- row_group_rows
rows per Parquet row group of the indicator layer files (default 10000). A group holds at most 2 x
row_group_rows: DuckDB rounds to 2,048-row vectors and may go past the target (v20261012: 12,045 at most). What a reader may rely on is the order and the disjointcell_idstatistics. Recorded inrelease.json(row_group_rows, and the largest group written asrow_group_rows_max).- overwrite
replace an existing
dir_out/release/(default FALSE: stop).- decades
decades (first year) to export in the decade layers; default 1960-2020. NULL exports every decade present.
- esn
expected sample size for ES(n) in the decade layers; default 50 (as in the store).
- place_collections
optional named list (or character vector), slug = the gazetteer collection's membership (a directory or URL, its
membership.json, or anythingobis_place_indicators()takes asmembership), e.g.list(eez = "<gazetteer base>/eez"). DefaultNULL: no place layer.- place_exact
optional named list (or character vector), slug = the path of the collection's GeoParquet of polygons (
place_id, WKBgeometry, an optionalarea_km2), e.g.list(mr_eez = "<places-internal>/mr_eez/places.parquet"). DefaultNULL: no exact place layer. Needsplace_exact_occ.- place_exact_occ
the occurrence parquet path(s)/glob(s) the exact place indicators are computed from (OBIS open data, nested or flat; see
obis_place_indicators_exact()).- temp_dir, max_temp_dir_size
DuckDB spill directory and cap for the exact place computation (it runs in its own DuckDB instance with this
memory_limitandthreads).- memory_limit
optional DuckDB
memory_limit(e.g."24GB").- threads
optional DuckDB thread count (e.g.
8).- verbose
print progress and the summary (default TRUE).
Details
<dir_out>/<release>/
release.json build metadata, row counts, layout, columns, file manifest
all/res=<1..5>/data_0.parquet idx_h3 (all taxa, all years)
all/res=<6,7>/p=<parent>/data_0.parquet the fine res, one file per parent cell
eov/eov=<eov>/res=<r>/data_0.parquet idx_h3_eov
taxon/rank=<rank>/taxon=<taxon>/res=<r>/ idx_h3_taxon (phylum / class / order / family),
plus the heads (kingdom, subphylum, superclass ...)
decade/all/decade=<yyy0>/res=<r>/ per decade, all taxa (res in res_decade)
decade/eov/eov=<eov>/decade=<yyy0>/res=<r>/ per decade and EOV (res in res_decade)
year/all/year=<yyyy>/res=<r>/ per year, all taxa (res in res_year)
year/eov/eov=<eov>/year=<yyyy>/res=<r>/ per year and EOV (res in res_year)
species/res=<r>/class=<class>/data_0.parquet species x hex x count (res in res_species)
species/species.parquet the species lookup: names, name_key, lineage, eov
species/lineage.parquet aphiaid, name_key, lineage only (0.12.0)
species/heads.json the pre-aggregated heads and the layer serving each
species_decade/res=<r>/decade=<d>/class=<class>/ per decade (res in res_species_decade)
species_flat/res=<r>.parquet one cell-sorted file, every class (res_species_flat)
species_flat/res=<r>/p=<parent>.parquet the same split by parent cell (species_flat_split)
place/coll=<slug>/data_0.parquet per-place pooled indicators (only with place_collections)
place_exact/coll=<slug>/indicators.parquet, years.parquet, meta.json
exact point-in-polygon indicators (only with place_exact)
collection.json STAC Collection (h3 profile), obis_h3_stac_collection()
stats.parquet value ranges per layer x key x period x res x variable
taxon_groups.parquet the taxa and EOVs available, with totals and paths
files.parquet manifest: layer, path, url_path, rows, bytes per fileEvery indicator file has the columns h3 (lowercase hex string), cell_id,
n, sp, shannon, simpson, es; the partition keys live in the path
(read with hive_partitioning = true). Its rows are sorted by cell_id in
row groups of row_group_rows (about 10k), so a reader that knows the cells
under a tile reads only the row groups whose cell_id statistics overlap
them (a range read) instead of the whole file. Each file is written by its
own COPY (obis_h3_write_partitions()), since DuckDB's PARTITION_BY
does not keep the order. The decade layers are computed from
occ_h3 with the same SQL builder as idx_h3 / idx_h3_eov (so the math is
identical to the store): n = records, sp = distinct species, Shannon,
Simpson and ES(esn), grouped by cell and decade = floor(date_year / 10) * 10; records without a year are left out of the decade layers. EOV membership
is the store's eov table (occ_h3.aphiaid = eov.taxonID), as in
obis_eov_bake(). Layers whose source tables are missing (e.g. no
idx_h3_eov in a store without taxonomy) are skipped with a message.
Partition values are percent-encoded by DuckDB (a space becomes %20), and
DuckDB decodes them on read. A static web server decodes the URL once, so a
browser must request the on-disk name with % itself encoded (%2520);
taxon_groups.parquet carries both path (on disk) and url_path (to
fetch), and files.parquet lists every file with both forms. Each partition
is exactly one data_0.parquet.
Parent partitions for the fine resolutions. A whole res-7 layer is tens
of MB, too much for a browser to load at once, so the resolutions named in
parent_res are split by parent cell: every row of all/res=7/ goes to
all/res=7/p=<h3 of its res-3 parent>/data_0.parquet (default res 6 by its
res-2 parent, res 7 by its res-3 parent), and a viewer loads only the parent
cells that cover its viewport. The taxon layer uses parent_res_taxon
(default the res-0 base cell: res-3 parents would make over a million
files across ~5k taxa). A (layer key, res) with at most parent_min_rows
rows stays one whole file, since splitting it would only add requests. The
decade layers are never split (their default resolutions are 1-5).
files.parquet has a parent column (the partition's parent cell, NULL for
a whole file; its resolution is the parent resolution), and each layer in
release.json lists the parent_res actually used.
Place layer (optional). With place_collections, each gazetteer
collection gets one file, place/coll=<slug>/data_0.parquet, with the
indicators of every place pooled from its res-7 member cells, records
weighted by each cell's share of area inside the place
(obis_place_indicators()): columns place_id, layer (all | eov),
key (” or the EOV), decade (NULL = all years, else the decades above),
n (fractional), sp, shannon, simpson, es, cells, area. These
are true per-place indices of the pooled, area-weighted counts, not means of
hexagon values. The files are listed in files.parquet (layer place) and
release.json (layers$place, with each collection's membership version);
stats.parquet does not cover them.
Exact place layer (optional). With place_exact, each polygon collection gets
place_exact/coll=<slug>/indicators.parquet and years.parquet
(obis_place_indicators_exact(), written by obis_place_exact_write()):
the indicators of every place computed from the OBIS occurrences inside the true
polygon, with no H3 approximation, per EOV (all and the store's eov table), per
decade and per year, with Berger-Parker and density. They are values only (no
geometry), computed from the occurrence parquet files place_exact_occ (the local
snapshot the store was built from), which takes far longer than the layers above.
The files are listed in files.parquet (layer place_exact) and release.json
(layers$place_exact, with each collection's places and record totals);
stats.parquet does not cover them.
Year layers. year/all and year/eov are the decade layers per year:
the same SQL (obis_h3_year_sql()) grouped by year = date_year, one
partition per year present from min(date_year) to max(date_year) (or
the years asked for), at the resolutions res_year (default 1-5, never
split by parent). release.json lists the years written (years).
The species table (species/, spike 2 of the Ocean Metrics
restructure): one row per (cell, aphiaid) with n = records, at the
resolutions res_species (default 3, 4, 5; res 4 rolled up from the res-5
tier), so a browser answers a WoRMS subtree or "which species are in this
cell" from static files. Columns h3, cell_id, name_key, aphiaid, n
(obis_h3_species_sql()); one data_0.parquet per (res, class) written by
obis_h3_write_partitions(), sorted by cell_id, aphiaid in row groups of
100k; a NULL class is class=_none. species/species.parquet
(obis_h3_species_lookup_sql()) has one row per aphiaid with its names, a
name_key per distinct name (richness counts names) and its lineage
(self, then ancestors up the store's taxon table); a subtree is
list_contains(lineage, <aphiaid>) (obis_h3_species_subtree_sql()).
species/heads.json lists the taxa that need a pre-aggregated head
(obis_h3_species_heads(): Animalia and every taxon spanning two or more
named classes) and the layer that serves each. Each species row also
carries year_min and year_max, the first and last year the aphiaid was
recorded in the cell (within the decade in species_decade/), for the Cell
pane. species_flat/res=<r>.parquet (default res 1 to 3) is the same table in one
file for every class, sorted by cell_id, aphiaid in row groups of
row_group_rows, so the species of one cell are a range read of one file
(spike 2: 0.53 MB for a res-1 cell) instead of one row group per class file.
The finest of res_species_flat is written from the store (with h3); each
coarser one is it rolled up (obis_h3_write_species_flat(): n summed,
year_min the least, year_max the greatest, no h3), so a coarse
hexagon reads its own rows, not its children's. species_flat_split splits
a resolution by a parent cell, species_flat/res=3/p=<res-1 parent>.parquet
by default (no h3, row groups of split_row_group_rows), so a cell reads
a file whose footer comes with the first range read. The lookup
species/species.parquet carries each species' eov
(obis_h3_species_eov_sql(), the EOV layers' own membership join; NULL for
none) and species/lineage.parquet holds its aphiaid, name_key, lineage
alone (obis_h3_write_species_lookup()), both in row groups of
row_group_rows.
The heads (heads = TRUE) are written as ordinary taxon layers,
taxon/rank=<rank>/taxon=<name>/res=<r>/ at res_heads
(obis_h3_heads_sql(): the WoRMS subtree from the lineage, species counted by
name_key, so a head equals obis_h3_species_subtree_sql() cell for cell),
where rank is the WoRMS rank lowercased without a botanical alias
("Phylum (Division)" is phylum). They then appear in stats.parquet,
taxon_groups.parquet and files.parquet as any taxon. A head is not
written when a layer already serves it: one whose subtree holds every
aphiaid (and the store has no record without one) is the all layer; one
whose rank and name are a rank layer of idx_h3_taxon keeps that layer
(built from the OBIS rank column, not the WoRMS subtree, so it may differ in
a few cells). heads.json gives each head's served_by (head, all,
rank_layer, or homonym for a second taxon with the same rank and name)
and its layer. The per-decade variant
species_decade/ is written only with species_decades = TRUE. Rows with
no aphiaid are left out; a store without one (e.g. the demo store built from
occ_SAtlantic) gets no species layer.
collection.json is a STAC Collection of the release for the Ocean Metrics
h3 profile (obis_h3_stac_collection()).
stats.parquet (sorted by layer, key, in row groups of row_group_rows,
so a legend reads only the groups of its layer) has one row per indicator layer and (key, decade,
year, res, variable) with min, p02, p50, p98, max and
n_cells (cells with a non-NULL value; a variable NULL everywhere in a
partition gets a row with NULL ranges and n_cells 0), for colour ramps.
key is the EOV or taxon name (” for the all-taxa layers), rank the
taxon rank (” otherwise), decade NA outside the decade layers, year NA
outside the year layers.
Examples
if (FALSE) { # \dontrun{
con <- obis_store_connect("~/data/obis/obis_h3.duckdb")
obis_h3_export_parquet(con, "~/data/obis-h3", "v20260728")
} # }