catmap is a TypeScript visualization library for geotechnical monitoring systems. It provides time-series charts, cross-sections, borehole logs, data utilities, and React wrappers while keeping the core contracts framework-agnostic.
The library is split by responsibility: data parsing and decimation live in @catmap/data, chart renderers live in @catmap/charts, geotechnical views live in @catmap/geotech, and React bindings live in @catmap/react.
- Piezometer, settlement, rainfall-response, inclinometer, sensor-health, multi-instrument, and spectral waterfall charts
- Chart analysis tools for inspection, viewport zooming, panning, and reset callbacks
- SVG cross-section views and borehole logs
- Time-series decimation, bucket aggregation, in-memory data sources, and Arrow-compatible column loading
- uPlot time-series rendering, Canvas 2D spectral waterfall rendering, and a lightweight WebGL point renderer with optional OffscreenCanvas support
- Browser PNG/PDF export helpers for canvas and SVG output
- React wrappers for geotechnical charts, spectral waterfalls, cross-sections, and borehole logs
| Package | Purpose |
|---|---|
@catmap/core |
Framework-agnostic contracts for charts, layers, renderer adapters, events, plugins, and data sources |
@catmap/data |
Time-series types, data sources, decimation, aggregation, mock data, and Arrow-like column import |
@catmap/charts |
uPlot adapter, spectral waterfall chart, chart layer helpers, WebGL point renderer, and browser export utilities |
@catmap/geotech |
Domain APIs for geotechnical charts, cross-sections, and borehole logs |
@catmap/react |
React components wrapping @catmap/charts and @catmap/geotech APIs |
apps/playground |
Vite playground using mock geotechnical data |
This repository is a pnpm workspace. Inside the monorepo, packages consume each other through workspace:*.
pnpm install
pnpm devFor an external application, install only the packages you need once they are published:
pnpm add @catmap/react @catmap/charts @catmap/geotech @catmap/dataPeer/runtime dependencies depend on the package surface you use:
- React components require
react >=18. - Time-series charts require
uplotstyles to be loaded by the application.
import "uplot/dist/uPlot.min.css";import {
BoreholeLog,
CrossSectionView,
PiezometerChart,
SpectralWaterfallChart
} from "@catmap/react";
export function MonitoringDashboard() {
return (
<>
<PiezometerChart
instrument={{ id: "PZ-001", name: "Piezometer PZ-001" }}
readings={[
{ timestamp: Date.UTC(2026, 0, 1), waterLevel: 1210.4 },
{ timestamp: Date.UTC(2026, 0, 2), waterLevel: 1211.2 }
]}
thresholds={[
{ value: 1211.6, label: "Alert", severity: "warning" },
{ value: 1212.6, label: "Action", severity: "critical" }
]}
yAxis="waterLevel"
showThresholds
tools={{
onInspect: (event) => console.log(event),
onViewportChange: (range) => console.log(range)
}}
/>
<CrossSectionView
title="Section A"
series={[
{
id: "ground",
label: "Ground surface",
points: [
{ distance: 0, elevation: 1240 },
{ distance: 100, elevation: 1233 }
]
}
]}
/>
<BoreholeLog
boreholeId="BH-01"
waterLevel={10.5}
intervals={[
{ from: 0, to: 4, label: "Fill" },
{ from: 4, to: 12, label: "Silty sand" },
{ from: 12, to: 24, label: "Weathered rock" }
]}
/>
<SpectralWaterfallChart
title="Interactive waterfall spectral chart"
x={[0, 1, 2, 3]}
spectra={[
{ id: "s0", label: "Spectra 0", values: [0, 3, 1, 2] },
{ id: "s1", label: "Spectra 1", values: [1, 4, 2, 1] }
]}
height={420}
/>
</>
);
}import { PiezometerChart } from "@catmap/geotech";
const chart = new PiezometerChart(container, {
instrument: { id: "PZ-001", name: "Piezometer PZ-001" },
readings,
thresholds,
yAxis: "waterLevel",
showThresholds: true
});
chart.updateData(nextReadings);
chart.resetViewport();
chart.resize();
chart.destroy();import { SpectralWaterfallChart } from "@catmap/charts";
const waterfall = new SpectralWaterfallChart(container, {
x,
spectra,
height: 560,
onSelectionChange: (selection) => console.log(selection)
});
waterfall.updateData(nextSpectra);
waterfall.resize();
waterfall.destroy();import {
TimeSeriesDataSource,
bucketAggregation,
decimateTimeSeries,
timeSeriesFromArrow
} from "@catmap/data";
const source = new TimeSeriesDataSource(points);
const { data, total } = await source.query({
from: Date.UTC(2026, 0, 1),
to: Date.UTC(2026, 0, 31),
maxPoints: 600
});
const hourly = bucketAggregation(data, 60 * 60 * 1000);
const decimated = await decimateTimeSeries(points, { maxPoints: 1000, useWorker: true });
const fromArrow = timeSeriesFromArrow(arrowTable, {
timestampColumn: "timestamp",
valueColumn: "water_level",
qualityColumn: "quality",
timestampUnit: "ms"
});
console.log(total, hourly.length, decimated.length, fromArrow.length);timeSeriesFromArrow accepts Arrow-compatible table objects that expose getChild(name) and vector columns with get(index). It intentionally does not parse Arrow IPC files; parse those upstream and pass the table object into catmap.
import { canvasToPngBlob, svgToPdfBlob } from "@catmap/charts";
const png = await canvasToPngBlob(canvas);
const pdf = await svgToPdfBlob(svgElement, {
width: 900,
height: 500,
background: "#ffffff"
});Exports run in browser environments. Server-side export should use a browser renderer or a dedicated PDF/image pipeline.
pnpm install
pnpm dev
pnpm typecheck
pnpm test
pnpm buildWorkspace scripts:
pnpm dev: starts the Vite playground.pnpm typecheck: runs TypeScript checks across packages.pnpm test: runs Vitest across packages.pnpm build: builds package outputs with tsup and the playground with Vite.pnpm lint: runs ESLint with zero warnings.
See DEVELOPMENT.md for completed phases, design constraints, known limitations, and future work.
@catmap/corestays framework-agnostic and renderer-agnostic.- React components live only in
@catmap/react. - Concrete rendering engines stay behind package boundaries: uPlot and Canvas 2D in
@catmap/charts. - Large-data paths prefer typed arrays, Arrow-compatible columns, chunks, or tiles over object-heavy hot paths.
- WebGPU, Rust/WASM, backend tiling, and custom parsers should be added only after benchmarks show the current path is the bottleneck.
See LICENSE.