A GUI toolkit for exploring stable isotope data files read with isoreader2. It ships ready-to-run explorer apps, a Show code feature that writes the isoreader2 code to reproduce whatever you’re looking at, and a set of composable Shiny modules you can recombine into your own app.
# install the release version from CRAN
install.packages("isoexplorer")
# or the development version from GitHub
# install.packages("pak")
pak::pak("isoverse/isoexplorer")
# isoreader2 relies on an external helper executable; check/install it once:
isoreader2::ir_check_isoextract()There are two ways to launch an explorer.
The focused explorers take an in-memory ir_isofiles
object and show one measurement type at a time — a file-selector sidebar
on the left and the plot on the right:
library(isoexplorer)
iso <- isoreader2::ir_find_continuous_flow("path/to/files") |>
isoreader2::ir_read_isofiles()
ie_explore_continuous_flow(iso)
ie_explore_dual_inlet(iso)
ie_explore_scans(iso)
ie_explore_metadata(iso) # just the selector tableThe object may be mixed-type (each explorer filters to its own type).
By default these run detached: the app is launched
in a separate R process (via callr, with the object handed
over in a temporary .rds) and opened in your browser, so
your R session stays free. Closing the browser tab stops the app and its
process; the process is also killed if your R session exits, so nothing
is left running. Pass detached = FALSE to instead run the
app in the current session (blocking) with
shiny::runApp(); add launch = FALSE to get the
[shiny::shinyApp()] object back unrun (for deployment or manual
launching). Called while a document is being rendered
(knitr / Quarto), they don’t launch — they print a note to run
interactively.
These functions also accept:
variable_name — the name used for the object in the
generated Show code output. It defaults to the deparsed
expression you passed, so my_iso |> ie_explore_scans()
writes code starting from my_iso; override it with
ie_explore_scans(iso, variable_name = "special_name").initial_selection — a filter expression evaluated
against the aggregated metadata: FALSE (the default —
nothing selected), TRUE (everything), or any
[dplyr::filter()] condition such as
ie_explore_continuous_flow(iso, initial_selection = grepl("std", file_name)).
In detached mode it must be self-contained (re-evaluated in the separate
process).ie_create_isofiles_server() builds the full multi-tab
app (one tab per measurement type) with no
isofiles argument, and returns it as a [shiny::shinyApp()]
object to run (shiny::runApp()) or deploy. Data arrives at
runtime via:
ir_copy_examples() and reads them),upload_folder = "some/dir"): files of any isoreader2 type,
their .json serializations, and .zip archives
are stored, read, and added automatically, andmonitoring_folders = "some/dir"):
isofiles appearing there are picked up automatically.ie_create_isofiles_server() |> shiny::runApp() # Load examples / upload
ie_create_isofiles_server(upload_folder = "uploads", monitoring_folders = "incoming") |>
shiny::runApp()Only newly seen files are ever read, so adding files is cheap. A “get started” prompt is shown until something is loaded.
Shiny caps uploads at 5 MB per file by default; since raw isofiles
are often larger, raise it with max_upload_size (in
MB):
ie_create_isofiles_server(upload_folder = "uploads", max_upload_size = 200) |>
shiny::runApp()Behind a reverse proxy (e.g. nginx in front of ShinyProxy) you may
also need to raise that proxy’s own request-body limit (nginx:
client_max_body_size).
Every app has a Show code button (top-right of the navbar) that assembles, from the modules currently in view, the isoreader2 code that reproduces the plot — read → aggregate → plot — and opens it in a read-only editor.
ir_filter_metadata(...)), the chosen
species/masses/ratios, scan type, zoom window, and every plot option
below are reflected in the code.#, ##, ###).” ```{r} ” chunks),
Copy the displayed code to the clipboard, or
Download .qmd (the download adds the YAML front matter;
the viewer omits it).library(isoreader2), library(ggplot2)).For example, exploring continuous flow data with a couple of files selected and a non-default color might generate:
# Setup
library(isoreader2)
library(ggplot2)
# Read data files
# assumes all data files are in the 'data' folder in the working directory
iso_files <- ir_find_continuous_flow("data") |>
ir_read_isofiles()
## Aggregate data files
cf_data <- iso_files |>
ir_aggregate_isofiles(intensity_units = "mV") |>
ir_calculate_ratios()
### Plot continuous flow
cf_data |>
ir_filter_metadata(file_name == "my_run_1") |>
ir_plot_continuous_flow(
ratio = c("45/44", "46/44"),
facet = file_name,
color = factor(analysis)
) +
theme(
text = element_text(size = 14),
legend.position = "bottom"
)Each plot has, around the plot area, an intensity
units popover, a ratios popover,
per-species popovers (to show/hide individual masses
and, once ratios are calculated, the available isotope
ratios — e.g. 45/44 — both checked by
default), x-axis zoom controls (brush to zoom, then
pan/back/show-all), and a PDF download.
The ratios popover controls ir_calculate_ratios():
a Calculate ratios toggle and, when it is on, the
additive offsets add to numerator / add to
denominator (in the current intensity-unit family’s reference
unit, shown in brackets — [V] for
V/mV, [nA] for the current units,
[cps] for cps; pre-filled with the function
defaults and emitted as num_add.* /
denom_add.* in the generated code) plus a
Normalize toggle (normalizes each ratio group by its
median). Edits are staged: nothing takes effect until you
click Apply (green); Cancel (gray)
discards them and the popover reopens at the active settings.
The Plot Options sidebar adds:
(none)
or any of species / mass / trace
/ data_type (intensities vs. ratios) or a metadata column.
Numeric and date/time columns are wrapped in factor() when
used as a discrete aesthetic. Faceting defaults to
file_name; intensities and ratios are split into separate
panels automatically (the plot functions facet on data_type
whenever both are present, regardless of this choice).free /
fixed / free_x / free_y).The generated code reflects all of this: the aggregate step gains an
ir_calculate_ratios() call (with only the non-default
offsets / normalize_ratios = mean) when Calculate
ratios is on, and the mass/ratio selections drive the plot
functions’ mass = ... / ratio = ... arguments
— while de-selecting whole species (vs. individual masses) is reflected
as species = ... rather than mass = ....
The apps above are thin compositions of a few modules, all wired
through a single central ie_file_server
(selector and plot modules never talk to each other directly —
everything goes through the file server):
ie_file_server(id, get_isofiles, initial_selection, upload_folder, monitoring_folders, examples_folder)
— the hub. Maintains a running set of read isofiles (seeded from
get_isofiles(), grown by uploads / watched folders — only
new files are read), splits it by measurement type, owns the
shared intensity units and the per-type selection, and serves metadata +
selection-filtered aggregated data. With upload_folder /
examples_folder set it drives the navbar Upload / Load
examples buttons (placed with ie_file_ui(id)).ie_metadata_ui(id) +
ie_scans_metadata_server(id, file) (and cf_ /
di_ variants). Pushes the chosen files/analyses into the
file server.ie_scans_plot_ui(id) +
ie_scans_plot_server(id, file) (and cf_ /
di_ variants). Pulls the selection-filtered data back
out.ie_code_ui(id) (the Show
code button) + ie_code_server(id) (see below).library(shiny)
library(isoexplorer)
ui <- bslib::page_fillable(
# convenience: selector sidebar + plot for one type
ie_type_explorer_ui("meta", ie_scans_plot_ui("scan"))
)
server <- function(input, output, session) {
file <- ie_file_server("files", get_isofiles = reactive(iso))
ie_scans_metadata_server("meta", file) # selection -> file server
ie_scans_plot_server("scan", file) # file server -> plot
}
shinyApp(ui, server)Want full control of the layout? Skip
ie_type_explorer_ui() and place
ie_metadata_ui("meta") and
ie_scans_plot_ui("scan") wherever you like. To reuse the
package’s navbar shell (theme picker, dark mode, about popup, Show code
button), launch with ie_run_app() instead of
shinyApp().
A custom module just needs to talk to the file handle — e.g. read
file$get_aggregated_scans_data() for the selected data, or
call file$set_units("nA") to change the aggregation units
app-wide.
ie_file_server
handleie_file_server() returns a list of accessors that every
module talks through. For each <type> in
scans / cf / di:
| accessor | purpose |
|---|---|
get_units(), set_units(units) |
the shared intensity units (default "mV") |
get_<type>_metadata() |
reactive metadata tibble for a selector table |
set_selected_<type>(rows) |
push the selected metadata rows (selectors call this) |
get_<type>_selection() |
the resolved current selection |
get_aggregated_<type>_data() |
reactive selection-filtered aggregated data (plots read this) |
get_<type>_select_signal() |
file paths an upload wants selected (a selector reflects this) |
get_active_type() |
the type whose tab to activate after an upload auto-select |
The typed wrappers (ie_scans_metadata_server() etc.)
bind all of these for you; you only need them directly when wiring a
custom module against the file server.
ie_code_server)ie_run_app() instantiates
ie_code_server("code") for you and passes it to
setup_modules as its second argument;
ie_code_ui("code") is the navbar button. Each step of the
document is a get_code generator that the wiring
registers into a dependency tree. The bundled apps
register, per measurement type, a
read -> aggregate -> plot chain:
setup_modules = function(file, code) {
ie_scans_metadata_server("scans_meta", file) # the selection table
code$register("read", "Read data files", get_code = function(input_var = NULL) {
list(
code = 'iso_files <- ir_find_scans("data") |> ir_read_isofiles()',
output = "iso_files"
)
})
code$register("agg", "Aggregate data files", depends_on = "read",
get_code = function(input_var = NULL) {
list(code = sprintf("scans <- %s |> ir_aggregate_isofiles()", input_var),
output = "scans")
})
plot <- ie_scans_plot_server("scans", file)
code$register("plot", "Plot scans", plot$get_code, depends_on = "agg")
}register(code_id, heading, get_code, depends_on = NULL, group = NULL)
places the snippet in the dependency tree. The document is rendered
depth-first, with the tree depth setting the heading
level; an optional group restricts a registration to one
navbar tab. A get_code is
function(input_var = NULL) returning
list(code = <string>, output = <string or NULL>):
it is called with the output variable of the module it depends on, and
its own output becomes the input variable of its dependents
(a terminal node such as a plot returns output = NULL).
This package is part of the isoverse suite of data tools for stable isotopes. If you like the functionality that isoverse packages provide, please help us spread the word and include an isoverse or individual package logo on one of your posters or slides. All logos are posted in high resolution in this repository. If you have suggestions for new features or other constructive feedback, please let us know on this short feeback form.
This project is supported by a grant from the US National Science Foundation (EAR-2411458) to Sebastian Kopf.