A monorepo containing Vue 3 visualization components for the Pennsieve platform.
| 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 |
| 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.
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
- Node.js >= 18
- pnpm >= 8
-
Clone the repo
git clone https://github.com/Pennsieve/Pennsieve-Visualization.git cd Pennsieve-Visualization -
Install pnpm (if you don't have it)
npm install -g pnpm
-
Install dependencies
pnpm install
-
Build all packages — this is required before running the dev server, because
coreimports frommicro-ctandts-viewerand needs their built output:pnpm build
-
Start the dev server
pnpm dev
Opens at
http://localhost:5173and servespackages/core/src/App.vueas a playground for testing components.
Why do I need to build first? The core package imports components from
@pennsieve-viz/micro-ctand@pennsieve-viz/tsvieweras dependencies. Without building them, those imports will fail and the dev server won't start.
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/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 devThe 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.
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 packagesThis 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 npmFor a single package manually:
pnpm --filter @pennsieve-viz/core build
cd packages/core && npm publish --access publicSee ARCHITECTURE.md for the standard factory Pinia store pattern that all @pennsieve-viz packages follow. This covers:
- The
createViewerStore/clearViewerStore/useViewerControlsAPI contract - How the host app wires up viewers to side panels via a thin composable
- Multi-instance support
- Implementation templates for new packages
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 neuroglancerDataExplorer and UMAP use DuckDB for in-browser SQL queries. Your app must provide a DuckDB store via Vue's provide/inject:
-
Install DuckDB:
pnpm add @duckdb/duckdb-wasm
-
Create a DuckDB store that implements the
DuckDBStoreInterface(seesrc/store/duckdbStore.jsfor a reference implementation). -
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 '@pennsieve-viz/core/style.css'<script setup>
import { DataExplorer, UMAP, Markdown, TextViewer, NiiViewer } from '@pennsieve-viz/core'
import { OrthogonalFrame } from '@pennsieve-viz/core'
</script><script setup>
import {
DataExplorerLazy,
UMAPLazy,
MarkdownLazy,
TextViewerLazy,
// These lazy-load from their respective packages:
TSViewer,
OmeViewer,
TiffViewer,
OrthogonalViewer
} from '@pennsieve-viz/core'
</script><script setup>
import { AiPlotly, ProportionPlot } from '@pennsieve-viz/plot'
</script>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 |
Renders NIfTI (.nii, .nii.gz) volumes using Niivue. Also supports NIfTI-Zarr for chunked streaming of larger files.
<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 |
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, useOrthogonalFrame(Neuroglancer) orOmeViewer(Viv) which do true viewport-based tile streaming.
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:
- Calls
/packages/{pkgId}/viewto get viewer assets - If a
.zarrview asset exists → returns its URL withzarrLevel: 0 - If only a source file exists and it's < 200MB → returns the
.nii.gzURL directly - If the source file is ≥ 200MB with no zarr asset → sets
needsConversion: true
pnpm add vue pinia element-plus
pnpm add @aws-amplify/auth # optional, for authenticated endpointsimport '@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) |
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()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.
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/webglimport '@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:
OmeOrthogonalVieweris deprecated and should not be used for new work. UseOrthogonalFrame(Neuroglancer via iframe) for orthogonal zarr viewing.
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.
-
Create a folder in
packages/core/src/my-component/withindex.tsandMyComponent.vue -
Export from
packages/core/src/index.ts:export * from './my-component' export const MyComponentLazy = defineAsyncComponent( () => import('./my-component').then(m => m.MyComponent) )
-
Test in
packages/core/src/App.vuewithpnpm dev