Explore slide results with Shiny

Start a local explorer

Install the optional shiny package, then run one of these commands in an interactive R session. The first starts empty; loading the demonstration is an explicit action. The second opens deterministic synthetic measurements and keeps the demonstration label visible.

bs_app()
bs_app(bs_example())

Stop the app with R’s stop button or Escape/Ctrl+C in the console. The app displays recorded measurements and linked slide images. It does not edit annotations, run inference, train a model, or save changes to the supplied result object.

Browse annotated slides

The viewer accepts any organ labels present in the results. The built-in viewer example includes liver, spleen, lung and kidney, and generates only synthetic images and measurements:

demo <- bs_viewer_example()
unique(demo$results$slides$organ)
#> [1] "Liver"  "Spleen" "Lung"   "Kidney"
demo$images[, c("image_id", "slide_id", "label")]
#>   image_id slide_id          label
#> 1  image-1 DEMO-001  Liver preview
#> 2  image-2 DEMO-002  Liver preview
#> 3  image-3 DEMO-003 Spleen preview
#> 4  image-4 DEMO-004 Spleen preview
#> 5  image-5 DEMO-005   Lung preview
#> 6  image-6 DEMO-006   Lung preview
#> 7  image-7 DEMO-007 Kidney preview
#> 8  image-8 DEMO-008 Kidney preview
bs_app(demo$results, images = demo$images, title = "My slide collection")

Use Previous/Next or the Slide selector to move through the filtered slides. Choose an image/layer when a slide has several regions or a raw/overlay pair. Drag to pan, scroll or use the zoom buttons, and choose Fit image to return to the whole image. Click a vector annotation to see its saved properties and measurements; the annotation selector provides a keyboard alternative. A baked PNG overlay is viewable but its marks are not individually selectable without corresponding vector annotations.

The images manifest has one row per image, with these fields:

Field Meaning
image_id Unique character image identity
slide_id Matching slide_id or physical_slide_id in the results
path Existing PNG/JPEG preview or native whole-slide file
label Optional display name for the region or layer
annotations Optional saved QuPath GeoJSON or point CSV (x, y, optional annotation_id, plus recorded measurements)
scale_x, scale_y Positive annotation-to-image coordinate scales, default 1
offset_x, offset_y Annotation-to-image offsets in displayed image pixels, default 0

A CSV manifest resolves file paths relative to its own directory. A data frame uses paths relative to the R working directory. Only these explicitly supplied files are served to the session; paths in uploaded result tables do not grant access to server files.

Coordinates are in the displayed image’s pixel frame, with x rightward and y downward. QuPath exports use full-resolution image pixels. For a preview at one-eighth resolution, set scale_x = scale_y = 1/8. For crops, supply the corresponding offsets. The transform is display = annotation * scale + offset. No registration is inferred. Points, lines and polygons (including polygon holes and multipart geometries) are supported. Limit vector overlays to 10,000 objects and 20 MiB per image; use review regions or rendered overlays for dense fields.

Open native whole-slide images

Native tiled viewing uses OpenSlide and the bundled OpenSeadragon browser viewer. PNG/JPEG previews do not need Python. For native images, create an optional isolated Python environment and install the reader:

python3 -m venv slide-reader
slide-reader/bin/python -m pip install openslide-python openslide-bin Pillow

On Windows, use slide-reader/Scripts/python.exe. Then point slide_python to that executable (or set BLOODSPOTTR_PYTHON). No installation or network download is performed by the app.

images <- data.frame(
  image_id = "image-001", slide_id = "001",
  path = "/data/slides/slide-001.svs",
  annotations = "/data/annotations/slide-001.geojson",
  label = "Whole slide"
)
bs_app(results, images = images, slide_python = "/path/to/slide-reader/bin/python")

Support depends on the installed OpenSlide reader and the actual file, not its extension alone. Native Aperio SVS and preview PNG paths are exercised during local qualification. A QuPath .qpproj/.qpdata or TissueGnostics .aqproj is a project container, not an OpenSlide image: export the image and QuPath GeoJSON first. This viewer does not write back to the native project.

Customize and watch saved results

title, group_by and group_label adapt the interface to other tissue panels or a different metadata grouping. Labels and organ choices are not fixed:

results$slides$sample_type <- c("Brain", "Skin", "Heart", "Lung",
                              "Kidney", "Muscle", "Tumor", "Other")
bs_app(results, images = images, title = "My experiment",
       group_by = "sample_type", group_label = "Sample type")

Pass a saved result file path to enable measurement refresh, and use Refresh saved data or Watch saved files. Watch mode checks every two seconds; saving a new annotation export refreshes its properties without rerunning analysis. A failed result read retains the last successfully loaded measurements and displays an error. The original files remain unchanged. Watch mode follows the supplied result path, not an active QuPath editor’s unsaved state. Selecting a browser upload or demonstration stops following that original result file.

bs_app("/data/results/results.json", images = "/data/results/images.csv")

Prepare an upload

A CSV needs the following columns. physical_slide_id is accepted in place of slide_id; if both are supplied, they must agree. Optional organ and qc_stratum (or stratum) columns populate the filters.

slides <- data.frame(
  slide_id = c("001", "002", "003"),
  organ = c("Liver", "Liver", "Spleen"),
  tissue_area_mm2 = c(1, 9, 2),
  red_area_mm2 = c(0.1, 0, NA_real_),
  candidate_spots = c(10, 0, NA_real_)
)
slides
#>   slide_id  organ tissue_area_mm2 red_area_mm2 candidate_spots
#> 1      001  Liver               1          0.1              10
#> 2      002  Liver               9          0.0               0
#> 3      003 Spleen               2           NA              NA

Save this as CSV and select it with Import slide results. Files must be at most 20 MiB. Counts are checked as decimal text before conversion, so an integer outside the exact numeric range is rejected rather than rounded. Blank numeric cells and NA mean missing. Numeric-looking IDs retain leading zeros; the literal ID NA remains an identifier. JSON should use the package’s canonical export, with a slides table and schema version "1.0".

file <- tempfile(fileext = ".csv")
utils::write.csv(slides, file, row.names = FALSE, na = "")
readLines(file)
#> [1] "\"slide_id\",\"organ\",\"tissue_area_mm2\",\"red_area_mm2\",\"candidate_spots\""
#> [2] "\"001\",\"Liver\",1,0.1,10"                                                     
#> [3] "\"002\",\"Liver\",9,0,0"                                                        
#> [4] "\"003\",\"Spleen\",2,,"
unlink(file)

An invalid upload displays the reason and retains the previous results. A successful upload resets filters. CSV downloads contain the filtered rows, including rows with missing measurements; formula-like text is escaped for spreadsheet viewing. Use bs_export_results() and its canonical JSON when exact metadata round-tripping is required.

Interpret the four cards

The cards show selected slide count, summed tissue area, pooled red-positive percentage and pooled candidate density. Rates divide summed numerators by summed support. They do not average percentages across slides.

For the two liver slides above, the red-positive percentage is 100 * (0.1 + 0) / (1 + 9) = 1%, and the candidate density is (10 + 0) / (1 + 9) = 1/mm². The unweighted mean of their percentages would be 5%, which answers a different question.

old <- par(mar = c(5, 4, 1, 1))
barplot(c("Pooled by tissue area" = 1, "Unweighted slide mean" = 5),
        col = c("#185FA5", "#A3ADB5"), ylab = "Red-positive tissue (%)",
        ylim = c(0, 6), las = 1, border = NA)

Two bars compare the correct pooled liver red-positive percentage of one percent with the unweighted mean of five percent.

par(old)

Selecting all three rows makes both pooled rates unavailable because the spleen measurements are unknown. Missing is not zero. A slide with zero tissue support also has undefined rates. The completeness line explains how many selected rows can appear in the plot; the cards still account for every selected row.

Scientific and hosting boundaries

A candidate event is not necessarily one erythrocyte. QC labels are supplied review decisions, not automatic diagnoses; low signal does not establish a negative control. Filtering changes descriptive support and must be recorded if used for a report. Biological replication, animal-level inference and validation require a separate study design.

The app is intended for a local R session. If you deploy it to a server, uploads are sent to that server; “local session” is not a confidentiality guarantee for a hosted deployment. Authentication, access control and retention policies belong to the hosting environment. Uploaded CSV/JSON is parsed as data, not executed.