Skip to content

Pennsieve/Pennsieve-Visualization

Repository files navigation

Pennsieve Visualization

A monorepo containing Vue 3 visualization components for the Pennsieve platform.

Packages

Package Description
@pennsieve-viz/core Main library — DataExplorer, UMAP, CSVViewer, Markdown, TextViewer, NiiViewer, OrthogonalFrame
@pennsieve-viz/plot Plotly-based components — AiPlotly, ProportionPlot (beta)
@pennsieve-viz/tsviewer Timeseries data viewer and annotator
@pennsieve-viz/micro-ct OmeViewer (OME-TIFF, OME-Zarr), TiffViewer (plain TIFF)
@pennsieve-viz/orthogonal Neuroglancer-based orthogonal viewer for OME-Zarr volumes

Viewer → Format Reference

Format Viewer Package Engine Notes
OME-Zarr (folder) OrthogonalFrame core (iframe → orthogonal) Neuroglancer 3D multi-panel orthogonal views. Viewport-based chunk streaming — handles arbitrarily large datasets.
OME-Zarr (folder) OmeViewer micro-ct Deck.gl + Viv 2D tiled, multi-channel, Z/T stacks
OME-Zarr (folder) NiiViewer core NiiVue Volume rendering. Loads entire volume into memory (capped by zarrMaxVolumeSize^3), so works for smaller neuroimaging OME-Zarrs but not large multi-resolution microscopy data.
OME-TIFF OmeViewer micro-ct Deck.gl + Viv 2D tiled, multi-channel
TIFF (tiled/multi-IFD) TiffViewer micro-ct GeoTIFF Pan/zoom, multi-channel. Works well with tiled TIFFs.
TIFF (single IFD) Too slow for TiffViewer (must decode entire image at once). Convert to zarr and use OmeViewer or OrthogonalFrame.
NIfTI (.nii.gz) NiiViewer core NiiVue Multi-slice and 3D render
CSV CSVViewer core Pure Vue Paginated table
CSV, Parquet DataExplorer core DuckDB In-browser SQL, export
CSV, Parquet UMAP core DuckDB + WebGL Interactive scatterplot
Parquet AiPlotly plot (beta) Plotly + DuckDB AI-driven plot generation
CSV, Parquet ProportionPlot plot (beta) Plotly + DuckDB Stacked bar charts
Markdown Markdown core marked.js Edit/preview modes
Plain text TextViewer core Pure Vue Line numbers, syntax types
Time-series TSViewer ts-viewer Canvas WebSocket streaming, annotations

OmeOrthogonalViewer (micro-ct) is deprecated — it does not handle multi-panel zarr data well. Use OrthogonalFrame (Neuroglancer) for orthogonal zarr viewing instead.

NiiViewer vs OrthogonalFrame for OME-Zarr: NiiVue loads the full volume into GPU memory, which is fast for small-to-medium neuroimaging data (up to ~1GB). For large multi-resolution microscopy OME-Zarrs, Neuroglancer (via OrthogonalFrame) is required — it streams only the chunks visible at the current zoom/position.

Folder Structure

pennsieve-visualization/
├── packages/
│   ├── core/              # @pennsieve-viz/core
│   │   └── src/
│   │       ├── csv-viewer/
│   │       ├── data-explorer/
│   │       ├── umap/
│   │       ├── markdown/
│   │       ├── text-viewer/
│   │       ├── NiiViewer/         # NIfTI viewer (Niivue) + useNiiSource composable
│   │       ├── orthogonal/       # OrthogonalFrame (iframe wrapper)
│   │       ├── duckdb/           # DuckDB interface types
│   │       └── composables/
│   ├── plot/              # @pennsieve-viz/plot (beta)
│   │   └── src/
│   │       ├── ai-plotly/
│   │       └── proportion-plot/
│   ├── orthogonal/        # @pennsieve-viz/orthogonal
│   ├── ts-viewer/         # @pennsieve-viz/tsviewer
│   └── micro-ct/          # @pennsieve-viz/micro-ct
├── src/                   # Dev playground app (not published)
│   ├── store/duckdbStore.js
│   └── main.js
├── pnpm-workspace.yaml
└── package.json

Requirements

  • Node.js >= 18
  • pnpm >= 8

Getting Started

  1. Clone the repo

    git clone https://github.com/Pennsieve/Pennsieve-Visualization.git
    cd Pennsieve-Visualization
  2. Install pnpm (if you don't have it)

    npm install -g pnpm
  3. Install dependencies

    pnpm install
  4. Build all packages — this is required before running the dev server, because core imports from micro-ct and ts-viewer and needs their built output:

    pnpm build
  5. Start the dev server

    pnpm dev

    Opens at http://localhost:5173 and serves packages/core/src/App.vue as a playground for testing components.

Why do I need to build first? The core package imports components from @pennsieve-viz/micro-ct and @pennsieve-viz/tsviewer as dependencies. Without building them, those imports will fail and the dev server won't start.

Rebuilding after changes

pnpm dev only hot-reloads changes inside packages/core/. If you edit other packages, rebuild them and refresh:

pnpm build:micro-ct       # after editing packages/micro-ct/
pnpm build:tsviewer        # after editing packages/ts-viewer/
pnpm build:orthogonal      # after editing packages/orthogonal/

Orthogonal viewer development

The orthogonal viewer has its own dev server for the embed app:

# Run the embed app standalone
pnpm --filter @pennsieve-viz/orthogonal dev:embed

# To test with the core playground, run both:
# Terminal 1: embed app on port 5174
pnpm --filter @pennsieve-viz/orthogonal dev:embed
# Terminal 2: core playground on port 5173
pnpm dev

The viewer will automatically be deployed to non-prod by Jenkins when a PR is merged into main. Prod deployments are handled manually by running a service-deploy job in Jenkins.

Build

pnpm build                    # micro-ct + tsviewer + core (all packages)
pnpm build:core               # @pennsieve-viz/core only
pnpm build:micro-ct            # @pennsieve-viz/micro-ct only
pnpm build:tsviewer            # @pennsieve-viz/tsviewer only
pnpm build:orthogonal          # @pennsieve-viz/orthogonal library
pnpm build:orthogonal-embed    # orthogonal embed app (dist-embed/)
pnpm clean        # Remove all dist folders
pnpm lint         # Lint all packages
pnpm type-check   # Type check all packages

Publishing to npm

This monorepo uses Changesets for version management.

pnpm changeset    # Create a changeset (select packages + change type)
pnpm version      # Update versions and changelogs
pnpm release      # Build + publish to npm

For a single package manually:

pnpm --filter @pennsieve-viz/core build
cd packages/core && npm publish --access public

Architecture

See ARCHITECTURE.md for the standard factory Pinia store pattern that all @pennsieve-viz packages follow. This covers:

  • The createViewerStore / clearViewerStore / useViewerControls API contract
  • How the host app wires up viewers to side panels via a thin composable
  • Multi-instance support
  • Implementation templates for new packages

Usage in a Consuming App

Install

pnpm add @pennsieve-viz/core

# Optional viewer packages (only needed if importing directly, not via core lazy exports)
pnpm add @pennsieve-viz/tsviewer
pnpm add @pennsieve-viz/micro-ct
pnpm add @pennsieve-viz/orthogonal neuroglancer

DuckDB Setup (required for DataExplorer & UMAP)

DataExplorer and UMAP use DuckDB for in-browser SQL queries. Your app must provide a DuckDB store via Vue's provide/inject:

  1. Install DuckDB:

    pnpm add @duckdb/duckdb-wasm
  2. Create a DuckDB store that implements the DuckDBStoreInterface (see src/store/duckdbStore.js for a reference implementation).

  3. Provide it in your app entry:

    import { createApp } from 'vue'
    import { createPinia } from 'pinia'
    import { useDuckDBStore } from './store/duckdbStore'
    
    const app = createApp(App)
    const pinia = createPinia()
    app.use(pinia)
    
    const duckdbStore = useDuckDBStore()
    app.provide('duckdb', duckdbStore)
    
    app.mount('#app')

Components that don't use DuckDB (Markdown, TextViewer, OrthogonalFrame, TSViewer) work without this setup.

Import Styles

import '@pennsieve-viz/core/style.css'

Components

Direct imports (production-ready)

<script setup>
import { DataExplorer, UMAP, Markdown, TextViewer, NiiViewer } from '@pennsieve-viz/core'
import { OrthogonalFrame } from '@pennsieve-viz/core'
</script>

Lazy-loaded (tree-shaking)

<script setup>
import {
  DataExplorerLazy,
  UMAPLazy,
  MarkdownLazy,
  TextViewerLazy,
  // These lazy-load from their respective packages:
  TSViewer,
  OmeViewer,
  TiffViewer,
  OrthogonalViewer
} from '@pennsieve-viz/core'
</script>

Beta components (from @pennsieve-viz/plot)

<script setup>
import { AiPlotly, ProportionPlot } from '@pennsieve-viz/plot'
</script>

OrthogonalFrame

OrthogonalFrame wraps the Neuroglancer viewer in an iframe for full isolation. Point it at the Pennsieve-hosted embed app (or your own):

<OrthogonalFrame
  :source="zarrUrl"
  layout="4panel"
  :embed-url="'https://your-cloudfront-domain.com/embed.html'"
  @ready="onReady"
  @error="onError"
/>
Prop Type Default Description
source string OME-Zarr source URL (required)
layout '4panel' | '3d' | 'xy' | 'xz' | 'yz' '4panel' Viewer layout
embedUrl string '/' Base URL of the hosted embed app

NiiViewer

Renders NIfTI (.nii, .nii.gz) volumes using Niivue. Also supports NIfTI-Zarr for chunked streaming of larger files.

Basic usage

<script setup>
import { NiiViewer } from '@pennsieve-viz/core'
</script>

<template>
  <NiiViewer url="https://example.com/brain.nii.gz" />
</template>
Prop Type Default Description
url string URL to a .nii.gz or .zarr volume (required)
sliceType 'axial' | 'coronal' | 'sagittal' | 'multi' | 'render' 'multi' Slice layout
zarrLevel number Zarr pyramid level (0 = highest res). Enables chunked loading when set.
zarrMaxVolumeSize number Max voxels per dimension for zarr virtual volume
zarrChannel number Which channel to load from zarr

Large file strategy

NiiViewer loads the entire file into memory. This works well for files under ~200MB. For larger NIfTI files, convert them to zarr via a Pennsieve workflow and pass the zarr URL with zarrLevel:

<NiiViewer url="https://s3.../brain.ome.zarr" :zarr-level="0" :zarr-max-volume-size="512" />

Note: Niivue's zarr support loads a fixed-size virtual volume into memory (zarrMaxVolumeSize^3). This works for neuroimaging data (typically up to ~1GB). For multi-GB microscopy OME-Zarr, use OrthogonalFrame (Neuroglancer) or OmeViewer (Viv) which do true viewport-based tile streaming.

useNiiSource composable

useNiiSource resolves a Pennsieve package to a NiiViewer-ready URL. It checks for a zarr view asset first, falls back to the source file if it's small enough, or flags that conversion is needed.

This is intended for use in the consuming app (e.g. pennsieve-app), not in the viz library itself.

<script setup>
import { NiiViewer, useNiiSource } from '@pennsieve-viz/core'

const { url, zarrLevel, needsConversion, fileSizeMB, error } =
  useNiiSource({ pkgId: 'N:package:...', apiUrl, getToken })
</script>

<template>
  <NiiViewer v-if="url" :url="url" :zarr-level="zarrLevel" />
  <p v-else-if="needsConversion">
    File too large ({{ fileSizeMB }}MB). Run the NIfTI → Zarr conversion workflow.
  </p>
  <p v-else-if="error">{{ error }}</p>
</template>

The composable:

  1. Calls /packages/{pkgId}/view to get viewer assets
  2. If a .zarr view asset exists → returns its URL with zarrLevel: 0
  3. If only a source file exists and it's < 200MB → returns the .nii.gz URL directly
  4. If the source file is ≥ 200MB with no zarr asset → sets needsConversion: true

TSViewer

Peer Dependencies

pnpm add vue pinia element-plus
pnpm add @aws-amplify/auth  # optional, for authenticated endpoints

Usage

import '@pennsieve-viz/tsviewer/style.css'
<script setup>
import { TSViewer, createViewerStore, clearViewerStore } from '@pennsieve-viz/tsviewer'

// Create an isolated store instance (supports multiple viewers on one page)
const viewerStore = createViewerStore('my-viewer')

viewerStore.setViewerConfig({
  timeseriesDiscoverApi: 'https://api.pennsieve.io/timeseries'
})

viewerStore.fetchAndSetActiveViewer({ packageId: 'your-package-id' })

// Cleanup when done
onUnmounted(() => clearViewerStore('my-viewer'))
</script>

<template>
  <TSViewer :pkg="packageData" />
</template>
Prop Type Default Description
pkg Object {} Package metadata object
isPreview Boolean false Preview mode (no toolbar)
sidePanelOpen Boolean false Side panel state (affects layout)

Store API

const store = createViewerStore('instance-id')

store.setViewerConfig({ timeseriesDiscoverApi: '...' })
store.fetchAndSetActiveViewer({ packageId: '...' })

store.viewerChannels           // all channels
store.viewerSelectedChannels   // selected channels
store.setSelectedChannels([...])

store.viewerAnnotations
store.createAnnotation(annotation)
store.updateAnnotation(annotation)
store.deleteAnnotation(annotation)

store.resetViewer()

// Cleanup
clearViewerStore('instance-id')
clearAllViewerStores()

Controls Composable

For side panels and external control UIs, use useViewerControls instead of the raw store:

import { useViewerControls } from '@pennsieve-viz/tsviewer'

const controls = useViewerControls('instance-id')

controls.channels          // readonly ref
controls.selectedChannels  // readonly computed
controls.selectChannels(['ch-1', 'ch-2'])
controls.setActiveTool('annotate')
controls.reset()

See ARCHITECTURE.md for the full pattern.


Micro-CT

Peer Dependencies

pnpm add vue pinia @deck.gl/core @deck.gl/extensions @deck.gl/geo-layers @deck.gl/layers @deck.gl/mesh-layers @luma.gl/constants @luma.gl/core @luma.gl/engine @luma.gl/shadertools @luma.gl/webgl

Usage

import '@pennsieve-viz/micro-ct/style.css'
<script setup>
import { OmeViewer, TiffViewer } from '@pennsieve-viz/micro-ct'
</script>

<template>
  <OmeViewer :source="omeTiffUrl" source-type="ome-tiff" instance-id="my-ome" />
  <TiffViewer :source="tiffUrl" />
</template>
Prop Type Default Description
source string | File URL or File to load (required)
sourceType 'ome-zarr' | 'ome-tiff' 'ome-zarr' Source format
instanceId string 'default' Store instance ID for multi-viewer / side-panel support

Exports: OmeViewer, OmeViewerControls, TiffViewer, useOmeLoader, createViewerStore, clearViewerStore, useViewerControls.

Note: OmeOrthogonalViewer is deprecated and should not be used for new work. Use OrthogonalFrame (Neuroglancer via iframe) for orthogonal zarr viewing.

Store API

OmeViewer writes all its state (channels, Z/T slices, loading, errors) to a factory Pinia store keyed by instanceId. External components (side panels, palettes) read that same store via useViewerControls:

import { useViewerControls } from '@pennsieve-viz/micro-ct'

// Use the same instanceId passed to <OmeViewer instance-id="my-ome">
const controls = useViewerControls('my-ome')

controls.channels          // readonly ref — channel list
controls.currentZ          // readonly ref — current Z slice
controls.setCurrentZ(5)
controls.setChannelVisibility(0, false)
controls.reset()

See ARCHITECTURE.md for the full pattern.


Adding a New Component to Core

  1. Create a folder in packages/core/src/my-component/ with index.ts and MyComponent.vue

  2. Export from packages/core/src/index.ts:

    export * from './my-component'
    
    export const MyComponentLazy = defineAsyncComponent(
      () => import('./my-component').then(m => m.MyComponent)
    )
  3. Test in packages/core/src/App.vue with pnpm dev

About

Library for all of Pennsieve visualization tools

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors