JavaScript Guide
The baselode npm package provides data loading, desurveying, 2D strip-log visualisation, and a 3D scene renderer for drillhole and spatial datasets.
Requires: Node.js 18+ · peer dependencies: React 18+, Three.js, Plotly, PapaParse
npm install baselodePeer Dependencies
baselode relies on the following peer dependencies, which must be installed in your application:
npm install react react-dom three three-viewport-gizmo plotly.js-dist-min papaparseData Model
The JavaScript package exposes the same Baselode Open Data Model constants as the Python package.
import {
HOLE_ID, LATITUDE, LONGITUDE, ELEVATION,
AZIMUTH, DIP, FROM, TO, MID, DEPTH,
EASTING, NORTHING, CRS,
BASELODE_DATA_MODEL_DRILL_COLLAR,
BASELODE_DATA_MODEL_DRILL_SURVEY,
BASELODE_DATA_MODEL_DRILL_ASSAY,
DEFAULT_COLUMN_MAP
} from 'baselode';Column standardization
Like the Python loaders, the JS column utilities normalise source field names to the Baselode data model.
import { standardizeColumns, normalizeFieldName } from 'baselode';
const normalised = standardizeColumns(rawRows);
// e.g. "HoleId" → "hole_id", "RL" → "elevation"Data Loading
Collars, surveys, and assays
import { loadCollars, loadSurveys, loadAssays, assembleDataset } from 'baselode';
// Accepts a CSV text string or an array of row objects
const collars = loadCollars(collarsText);
const surveys = loadSurveys(surveysText);
const assays = loadAssays(assaysText);
const dataset = assembleDataset({ collars, surveys, assays });Assay-focused loaders
For large assay CSVs with multiple analyte columns:
import { loadAssayFile, loadAssayHole, buildAssayState } from 'baselode';
// Load metadata (hole IDs + column names) without parsing all rows
const meta = await loadAssayMetadata(csvText);
// Load assay data for a specific hole
const holeData = loadAssayHole(csvText, 'HOLE_001');Structural data
import { parseStructuralPointsCSV, parseStructuralCSV } from 'baselode';
const structuralPoints = parseStructuralPointsCSV(csvText);
// Returns an array of { hole_id, depth, dip, azimuth, alpha, beta, comments }Block model
import { parseBlockModelCSV, getBlockStats, filterBlocks } from 'baselode';
const blocks = parseBlockModelCSV(csvText);
const stats = getBlockStats(blocks, 'au_ppm');
const subset = filterBlocks(blocks, { property: 'au_ppm', min: 1.0 });Polygonal grade blocks
Grade blocks are closed polyhedral meshes — grade shells, geologic domains, or any volumetric solid defined by triangulated vertices. They are loaded from a structured JSON format:
import { loadGradeBlocksFromJson, addGradeBlocksToScene } from 'baselode';
// Parse and validate the JSON (accepts a parsed object or a JSON string)
const blockSet = loadGradeBlocksFromJson(json);
// Render into an existing THREE.Scene (e.g. from Baselode3DScene)
const group = addGradeBlocksToScene(scene.scene, blockSet);
// Returns a THREE.Group whose children are one THREE.Mesh per blockJSON schema (version "1.0")
{
"schema_version": "1.0",
"units": "m",
"blocks": [
{
"id": "HG",
"name": "High grade",
"vertices": [[0,0,0], [10,0,0], [10,10,0], [0,10,0],
[0,0,5], [10,0,5], [10,10,5], [0,10,5]],
"triangles": [[0,1,2],[0,2,3], [4,5,6],[4,6,7],
[0,1,5],[0,5,4], [1,2,6],[1,6,5],
[2,3,7],[2,7,6], [3,0,4],[3,4,7]],
"attributes": { "grade_class": "HG", "au_ppm": 4.2 },
"material": { "color": "#B02020", "opacity": 1.0 }
}
]
}| Field | Required | Description |
|---|---|---|
schema_version | yes | Must be "1.0" |
units | no | Coordinate units string (e.g. "m") |
blocks[].id | yes | Unique identifier |
blocks[].name | yes | Display name |
blocks[].vertices | yes | [[x,y,z], ...] array of 3-D vertex positions |
blocks[].triangles | yes | [[i,j,k], ...] zero-based triangle index triples |
blocks[].attributes | no | Arbitrary key-value metadata shown in selection panel |
blocks[].material.color | no | CSS hex colour (default #888888) |
blocks[].material.opacity | no | 0–1 opacity (default 1.0) |
Unified dataset (assays + structural)
import { parseUnifiedDataset } from 'baselode';
const unified = parseUnifiedDataset(assaysCsvText, structuralCsvText);
// Returns a combined array with a `_source` tag ('assay' | 'structural')Desurveying
parseSurveyCSV and desurveyTraces
import { parseSurveyCSV, desurveyTraces } from 'baselode';
const surveyTable = parseSurveyCSV(surveyCsvText);
const traces = desurveyTraces(collarsCsvText, surveyTable);
// traces: Map<holeId, TracePoint[]> where each point has { x, y, z, md, azimuth, dip }Low-level desurvey methods
import {
minimumCurvatureDesurvey,
tangentialDesurvey,
balancedTangentialDesurvey,
buildTraces
} from 'baselode';
// minimumCurvatureDesurvey is the industry standard (default)
const trace = minimumCurvatureDesurvey(collar, surveyRows, { step: 1.0 });Attaching assay positions to 3D traces
import { attachAssayPositions } from 'baselode';
const assaysWithXYZ = attachAssayPositions(assayRows, traces);
// Adds { x, y, z } to each assay row by interpolating the traceVisualization
Column classification
baselode classifies columns automatically for the strip-log renderer:
import { classifyColumns, DISPLAY_NUMERIC, DISPLAY_CATEGORICAL, DISPLAY_COMMENT, DISPLAY_TADPOLE } from 'baselode';
const classification = classifyColumns(rows);
// Returns { colName: DISPLAY_NUMERIC | DISPLAY_CATEGORICAL | DISPLAY_COMMENT | DISPLAY_TADPOLE | DISPLAY_HIDDEN }2D strip log (Plotly)

import { buildIntervalPoints, buildPlotConfig, getChartOptions, defaultChartType } from 'baselode';
const points = buildIntervalPoints(holeRows, 'au_ppm');
const chartType = defaultChartType('au_ppm', holeRows);
const config = buildPlotConfig(points, 'au_ppm', { chartType });
Plotly.newPlot('container', config.data, config.layout);React component — TracePlot
TracePlot renders a complete multi-track Plotly strip log for a single hole.
import { TracePlot } from 'baselode';
<TracePlot
rows={holeRows}
properties={['au_ppm', 'lithology', 'alpha']}
/>React hook — useDrillholeTraceGrid
For building full drill-hole comparison grids:
import { useDrillholeTraceGrid } from 'baselode';
function MyGrid({ holes, selectedProperty }) {
const { plots } = useDrillholeTraceGrid({ holes, property: selectedProperty });
return <div className="grid">{plots.map(p => <TracePlot key={p.holeId} {...p} />)}</div>;
}Tool UI
Baselode includes a JavaScript-only Tool UI entrypoint for rendering Baselode visualisations as structured tool results.
Install zod alongside Baselode when importing baselode/tool-ui; it is a required peer dependency for the Tool UI schemas and is not bundled.
npm install baselode zodThe integration follows Tool UI's schema-first rendering pattern:
- A backend AI SDK tool returns a structured Baselode visualisation JSON payload.
- A Zod schema validates the result in the frontend renderer.
- The renderer uses Baselode's existing Plotly strip-log and Three.js scene helpers inside the assistant conversation.
import { AssistantRuntimeProvider } from '@assistant-ui/react';
import { AssistantChatTransport, useChatRuntime } from '@assistant-ui/react-ai-sdk';
import { useBaselodeToolUi } from './toolkit.jsx';
import 'baselode/tool-ui/style.css';
export default function App() {
const runtime = useChatRuntime({
transport: new AssistantChatTransport({ api: '/api/chat' }),
});
const aui = useBaselodeToolUi();
return (
<AssistantRuntimeProvider runtime={runtime} aui={aui}>
{/* assistant-ui thread */}
</AssistantRuntimeProvider>
);
}The serializable result is intentionally compact:
{
id: 'strip-log-BLDD001',
hole: {
id: 'BLDD001',
points: [
{ from: 0, to: 12, au_ppm: 0.11, lithology: 'SAP' },
{ from: 12, to: 24, au_ppm: 0.35, lithology: 'BAS' },
],
},
tracks: [
{ property: 'au_ppm', label: 'Au ppm', displayType: 'numeric' },
{ property: 'lithology', label: 'Lithology', displayType: 'categorical' },
],
}The baselode/tool-ui entry exports:
import {
BaselodeStripLogToolUI,
Baselode3DSceneToolUI,
SerializableBaselodeStripLogSchema,
SerializableBaselode3DSceneSchema,
safeParseSerializableBaselodeStripLog,
safeParseSerializableBaselode3DScene,
} from 'baselode/tool-ui';Theming Tool UI chrome
Plotly-rendered strip logs use the selected Baselode Plotly template. The surrounding Tool UI chrome, including headers, controls, legends, error states, and 3D scene frames, uses CSS custom properties derived from the same Baselode light and dark palettes.
BaselodeStripLogToolUI uses light chrome by default and switches to dark chrome when template="baselode-dark". Baselode3DSceneToolUI uses light chrome by default and switches to dark chrome when background="black".
Override the CSS variables on a parent container when your app needs to theme Tool UI elements that Plotly templates do not cover:
.my-assistant-theme .baselode-tool-strip-log,
.my-assistant-theme .baselode-tool-3d-scene {
--baselode-tool-bg: #ffffff;
--baselode-tool-panel: #f8fafc;
--baselode-tool-ink: #1e293b;
--baselode-tool-ink-soft: #64748b;
--baselode-tool-grid: #e8e8e8;
--baselode-tool-line: #d0d0d0;
--baselode-tool-accent: #f59e0b;
--baselode-tool-muted-1: #94a3b8;
--baselode-tool-muted-2: #cbd5e1;
--baselode-tool-muted-3: #e2e8f0;
--baselode-tool-primary: #8b1e3f;
}Plotly templates
Baselode ships two built-in Plotly templates that can be applied to any strip log.
| Export | Appearance |
|---|---|
BASELODE_TEMPLATE / BASELODE_LIGHT_TEMPLATE | White background, Inter font, neutral grey grid |
BASELODE_DARK_TEMPLATE | Dark background (#1b1b1f), Inter font, subtle warm grid |
Pass a template to the low-level builder or to TracePlot:
import { buildPlotConfig, BASELODE_DARK_TEMPLATE } from 'baselode';
const config = buildPlotConfig({
points,
isCategorical: false,
property: 'au_ppm',
chartType: 'markers+line',
template: BASELODE_DARK_TEMPLATE, // omit to use the default light template
});
Plotly.newPlot('container', config.data, config.layout);With TracePlot:
import { TracePlot, BASELODE_DARK_TEMPLATE } from 'baselode';
<TracePlot
config={config}
graph={graph}
holeOptions={holeOptions}
propertyOptions={propertyOptions}
onConfigChange={handleChange}
template={BASELODE_DARK_TEMPLATE}
/>Building a custom template
A Baselode template is a plain object with a layout key (and optionally a data key for trace defaults) — the same shape as a Plotly template object. You do not need to register it anywhere; just pass it directly.
const MY_TEMPLATE = {
layout: {
paper_bgcolor: '#0f1117',
plot_bgcolor: '#0f1117',
font: { family: 'JetBrains Mono, monospace', color: '#e2e8f0', size: 13 },
colorway: ['#38bdf8', '#34d399', '#fb923c', '#f472b6', '#a78bfa'],
xaxis: {
showline: false,
showgrid: true,
gridcolor: '#1e293b',
tickfont: { color: '#94a3b8' },
},
yaxis: {
showline: false,
showgrid: true,
gridcolor: '#1e293b',
tickfont: { color: '#94a3b8' },
},
hoverlabel: {
bgcolor: '#1e293b',
bordercolor: '#38bdf8',
font: { color: '#e2e8f0', size: 12 },
},
},
};
const config = buildPlotConfig({ points, property, chartType, template: MY_TEMPLATE });Colour mapping
Automatic commodity colours
Baselode automatically detects commodity elements in column names and applies a matching colour. A column called Au_ppm, au_ppb, or AU will all render in gold; Cu_pct will render in copper-brown.
No configuration is required — pass the column name to buildPlotConfig and detection is automatic.
Built-in semantic colour maps
For categorical strip logs (geology codes, lithology, alteration) two built-in maps are available:
| Name | Contents |
|---|---|
'commodity' | 18 commodity elements (Au, Ag, Cu, Fe, Ni, …) |
'lithology' | ~30 common rock types (granite, basalt, shale, …) |
import { buildCategoricalStripLogConfig } from 'baselode';
const config = buildCategoricalStripLogConfig(rows, {
fromCol: 'from',
toCol: 'to',
categoryCol: 'geology_code',
colourMap: 'lithology', // use the built-in lithology map
});You can also look up individual values:
import { getColour, LITHOLOGY_COLOURS } from 'baselode';
const colour = getColour('granite', LITHOLOGY_COLOURS); // '#EF9A9A'Custom colour maps
Supply any plain object mapping category strings to CSS colour values:
const ALTERATION_COLOURS = {
'potassic': '#e53e3e',
'phyllic': '#d69e2e',
'propylitic': '#38a169',
'argillic': '#3182ce',
'silicification': '#805ad5',
};
const config = buildCategoricalStripLogConfig(rows, {
fromCol: 'from',
toCol: 'to',
categoryCol: 'alteration_type',
colourMap: ALTERATION_COLOURS,
});Lookup is case-insensitive, so "Potassic" and "potassic" both match. Categories absent from the map fall back to a built-in rotation palette.
Color scale
Continuous numeric color scales for 3D viewers and maps:
import { buildEqualRangeColorScale, getEqualRangeColor, ASSAY_COLOR_PALETTE_10 } from 'baselode';
const scale = buildEqualRangeColorScale(values, ASSAY_COLOR_PALETTE_10);
const colour = getEqualRangeColor(scale, 2.5); // '#...'3D Scene
Baselode3DScene


Baselode3DScene is a thin orchestrator that owns the WebGL context and delegates rendering to domain-specific modules (drillholeScene, blockModelScene, structuralScene). Use it directly or through the pre-built React wrapper.
import { Baselode3DScene } from 'baselode';
const scene = new Baselode3DScene();
scene.init(containerElement); // attach to a DOM container
// Drillholes (desurveyed trace objects)
scene.setDrillholes(holes, { selectedAssayVariable: 'au_ppm', assayIntervalsByHole });
// Block model
scene.setBlocks(blockRows, 'au_ppm', stats);
// Structural discs
scene.setStructuralDiscs(structuralRows, holes, { radius: 5, opacity: 0.75 });
// Click handlers
scene.setDrillholeClickHandler(({ holeId }) => console.log(holeId));
scene.setBlockClickHandler((blockRow) => console.log(blockRow));
// Cleanup
scene.dispose();React component — Baselode3DControls
Drop-in React component with orbit controls, a camera gizmo, and a controls panel:
import { Baselode3DControls } from 'baselode';
import 'baselode/style.css';
<Baselode3DControls
traces={traces}
structuralDiscs={discs}
colorBy="au_ppm"
/>React component — BlockModelWidget
Interactive 3D block model viewer:
import { BlockModelWidget } from 'baselode';
import 'baselode/style.css';
<BlockModelWidget
blocks={blocks}
colorProperty="grade"
/>3D payload builders
import { tracesAsSegments, intervalsAsTubes, annotationsFromIntervals } from 'baselode';
const segments = tracesAsSegments(traces);
const tubes = intervalsAsTubes(assays, { colorBy: 'au_ppm', radius: 2 });
const annotations = annotationsFromIntervals(assays);Structural disc builder
import { buildStructuralDiscs } from 'baselode';
const discs = buildStructuralDiscs(structuralPoints, traces);
// Returns Three.js-ready disc descriptors for each structural measurementPolygonal grade blocks — 3D rendering
addGradeBlocksToScene renders each block as a THREE.Mesh with flat-shaded MeshStandardMaterial and an edge-highlight LineSegments child (hidden by default, shown on selection).
import { Baselode3DScene, loadGradeBlocksFromJson, addGradeBlocksToScene } from 'baselode';
const scene = new Baselode3DScene();
scene.init(containerElement);
const blockSet = loadGradeBlocksFromJson(json);
const group = addGradeBlocksToScene(scene.scene, blockSet, { defaultOpacity: 0.85 });
// Register meshes so the built-in selection glow fires on click
scene.selectables = Array.from(group.children);Click selection and edge highlight
When a mesh is clicked the scene's built-in raycast handler applies a glow outline (OutlinePass) around the outer silhouette. Each mesh also carries a hidden LineSegments child built from EdgesGeometry — showing it on selection highlights every polyhedral edge explicitly:
// Show/hide the edge overlay when selection changes
group.children.forEach((mesh) => {
const edgeLines = mesh.children[0];
if (edgeLines) edgeLines.visible = mesh.userData.id === selectedId;
});mesh.userData contains { id, attributes } from the source JSON, available in the click callback.
Raster Overlays
A raster overlay drapes a georeferenced image (geology map, satellite photo, survey grid, etc.) as a flat plane in the 3D scene. The image is placed at a specified elevation and bounded to real-world coordinates so it aligns with drillhole traces.
Image format
Baselode3DScene uses Three.js's built-in TextureLoader, which supports formats the browser can decode natively: PNG, JPEG, WebP. GeoTIFF is not supported directly — convert it first:
# Using GDAL
gdal_translate -of PNG my_map_georeferenced.tif my_map.pngCoordinate system
Overlay bounds are expressed in the same local scene coordinate system as the drillhole traces (x = Easting offset, y = Northing offset, z = elevation in metres). If your traces are offset from a survey origin, subtract the same origin from the GeoTIFF corner coordinates:
// GeoTIFF corners in MGA zone 50 (metres):
// Upper Left: (693 545, 7 675 285)
// Lower Right: (700 469, 7 667 027)
// Scene origin chosen as centroid of drillhole collars, e.g. (697 007, 7 671 156):
const ORIGIN_E = 697_007;
const ORIGIN_N = 7_671_156;
const bounds = {
minX: 693_545 - ORIGIN_E, // ≈ -3 462
maxX: 700_469 - ORIGIN_E, // ≈ 3 462
minY: 7_667_027 - ORIGIN_N, // ≈ -4 129
maxY: 7_675_285 - ORIGIN_N, // ≈ 4 129
};Bounds can also be expressed as an origin + size object:
const bounds = { x: -3462, y: -4129, width: 6924, height: 8258 };Loading a raster overlay
import { createRasterOverlay } from 'baselode';
const layer = await createRasterOverlay({
id: 'geology-map', // unique identifier (auto-generated if omitted)
name: 'Regional geology', // display name
source: { type: 'url', url: '/maps/geology.png' },
bounds, // placement bounds in scene coordinates
elevation: 350, // Z position (metres above datum); default 0
opacity: 0.85, // initial opacity 0–1; default 1
visible: true, // initial visibility; default true
});
scene.addRasterOverlay(layer);Loading from a browser File object
When the user uploads a file via <input type="file">:
const [file] = event.target.files;
const layer = await createRasterOverlay({
source: { type: 'file', file },
bounds,
elevation: 350,
});
scene.addRasterOverlay(layer);Supplying a pre-built Three.js texture
import * as THREE from 'three';
const texture = new THREE.TextureLoader().load('/maps/geology.png');
const layer = await createRasterOverlay({
source: { type: 'texture', texture },
bounds,
});
scene.addRasterOverlay(layer);Runtime controls
Opacity, visibility, and elevation can be changed at any time without recreating the overlay:
scene.setRasterOverlayOpacity('geology-map', 0.5);
scene.setRasterOverlayVisibility('geology-map', false);
scene.setRasterOverlayElevation('geology-map', 400); // shift ZListing and removing overlays
// Retrieve a layer by id
const layer = scene.getRasterOverlay('geology-map');
// List all active overlays
const layers = scene.listRasterOverlays();
// Remove one overlay and free its GPU resources
scene.removeRasterOverlay('geology-map');
// Remove all overlays
scene.clearRasterOverlays();Complete example (React)
import { useEffect, useRef } from 'react';
import { Baselode3DScene, createRasterOverlay } from 'baselode';
const BOUNDS = { minX: -3462, maxX: 3462, minY: -4129, maxY: 4129 };
export default function MapScene({ holes }) {
const containerRef = useRef(null);
const sceneRef = useRef(null);
useEffect(() => {
const scene = new Baselode3DScene();
scene.init(containerRef.current);
sceneRef.current = scene;
scene.setDrillholes(holes, { preserveView: false });
createRasterOverlay({
id: 'geology',
source: { type: 'url', url: '/maps/geology.png' },
bounds: BOUNDS,
elevation: 350,
opacity: 0.8,
}).then((layer) => scene.addRasterOverlay(layer));
const onResize = () => scene.resize();
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
scene.dispose();
};
}, []);
return <div ref={containerRef} style={{ width: '100%', height: '100vh' }} />;
}Camera Controls
Programmatic camera control helpers for the 3D scene:
import {
fitCameraToBounds,
recenterCameraToOrigin,
lookDown,
pan, dolly,
setFov
} from 'baselode';
fitCameraToBounds(scene, bounds);
lookDown(scene);
setFov(scene, 45);Standalone Bundle
For non-React environments (e.g. embedding in a plain HTML page or a Python Dash iframe), baselode ships a standalone UMD/IIFE module that registers itself as window.baselode:
<script src="baselode-module.js"></script>
<script>
const scene = new window.baselode.Baselode3DScene(document.getElementById('canvas'));
</script>The standalone bundle is built from vite.standalone.js in the JS package and is copied to demo-viewer-dash/assets/ automatically:
cd javascript/packages/baselode
npm run build:module