Skip to content

Repository files navigation

jaxray logo

Labeled multi-dimensional arrays for JavaScript

npm versionCoverageLicenseTypeScript

A JavaScript/TypeScript implementation inspired by Python's xarray library

FeaturesInstallationQuick StartAPIExamples


Why jaxray?

Working with multi-dimensional labeled data in JavaScript shouldn't be painful. jaxray brings the power of Python's xarray to the JavaScript ecosystem, making it easy to work with scientific data, climate datasets, and any labeled arrays.

Perfect for:

  • 🌍 Climate and weather data analysis
  • 📊 Time series with multiple dimensions
  • 🗺️ Geospatial data processing
  • 🔬 Scientific computing in the browser
  • 📈 Large dataset streaming and processing

Features

  • Labeled Arrays: Named dimensions and coordinates like xarray
  • 🎯 Smart Selection: Select by labels with nearest neighbor, forward/backward fill
  • 🌊 Streaming: Process massive datasets chunk-by-chunk with progress tracking
  • 📦 Zarr Support: Read sharded Zarr stores directly from IPFS
  • 🔐 Encryption: Transparent XChaCha20-Poly1305 authenticated encryption for Zarr datasets
  • 🔒 Type-Safe: Full TypeScript support with complete type definitions
  • 💨 Memory Efficient: Stream large selections without loading everything
  • 🔄 Immutable: All operations return new instances

Installation

npm install @dclimate/jaxray

Quick Start

Creating a DataArray

import{DataArray}from'@dclimate/jaxray';// Simple 1D array with labeled coordinatesconsttemperatures=newDataArray([20,22,25,23,21],{dims: ['time'],coords: {time: ['Mon','Tue','Wed','Thu','Fri']},attrs: {units: 'celsius',description: 'Daily temperatures'}});console.log(temperatures.data);// [20, 22, 25, 23, 21]console.log(temperatures.shape);// [5]

Creating a 2D DataArray

constgridData=newDataArray([[1,2,3],[4,5,6]],{dims: ['y','x'],coords: {y: [0,10],x: [0,10,20]}});

Selecting Data

// Select by labelconstwednesday=awaittemperatures.sel({time: 'Wed'});console.log(wednesday.data);// 25// Select multiple valuesconstselected=awaittemperatures.sel({time: ['Mon','Wed','Fri']});console.log(selected.data);// [20, 25, 21]// Slice selectionconstmidweek=awaittemperatures.sel({time: {start: 'Tue',stop: 'Thu'}});console.log(midweek.data);// [22, 25, 23]// Select by integer positionconstbyIndex=awaittemperatures.isel({time: 2});console.log(byIndex.data);// 25

Nearest Neighbor Selection

jaxray supports xarray-style nearest neighbor lookups and interpolation methods:

constdata=newDataArray([10,20,30,40,50],{dims: ['x'],coords: {x: [0,5,10,15,20]}});// Find nearest coordinateconstnearest=awaitdata.sel({x: 7},{method: 'nearest'});console.log(nearest.data);// 20 (nearest to x=7 is x=5)// Forward fill (last value <= target)constffill=awaitdata.sel({x: 12},{method: 'ffill'});console.log(ffill.data);// 30 (last value where x <= 12 is x=10)// Backward fill (first value >= target)constbfill=awaitdata.sel({x: 12},{method: 'bfill'});console.log(bfill.data);// 40 (first value where x >= 12 is x=15)// With toleranceconsttolerant=awaitdata.sel({x: 7},{method: 'nearest',tolerance: 3});// Succeeds because distance is 2// This would throw an error (distance 7 > tolerance 2)awaitdata.sel({x: 13},{method: 'nearest',tolerance: 2});

Open an IPFS-hosted Zarr dataset with a single CID

jaxray can auto-detect whether a CID points to a sharded Zarr store or a HAMT-backed one. All you need is the CID:

import{Dataset,openIpfsStore}from'@dclimate/jaxray';constcid='bafy…';// Automatically detects the underlying layout and selects the correct storeconst{ type, store }=awaitopenIpfsStore(cid);console.log(`Detected store type: ${type}`);// "sharded" or "hamt"// Use the store with the regular Dataset APIconstds=awaitDataset.open_zarr(store);// Work with the dataset immediatelyconsole.log(ds.dataVars);constsnapshot=awaitds.sel({latitude: 45,longitude: 34,time: '2025-09-02T00:00:00Z'});console.log(snapshot.getVariable('temperature').values);

The helper uses dClimate's public gateway by default. Provide your own gateway or custom IPFS primitives when needed:

import{createIpfsElements,openIpfsStore}from'@dclimate/jaxray';awaitopenIpfsStore(cid,{gatewayUrl: 'https://ipfs.my-org.dev'});// or reuse your own IPFS primitivesconstcustomElements=createIpfsElements('https://ipfs.my-org.dev');const{ type, store }=awaitopenIpfsStore(cid,{ipfsElements: customElements});

HTTP/2 gateway connections in Node.js

Node.js 26 and newer negotiate HTTP/2 for TLS gateways automatically. On Node.js 24 and older, install undici@7 and pass an HTTP/2-enabled dispatcher to KuboCAS:

npm install undici@7
import{KuboCAS}from'@dclimate/jaxray';import{Agent}from'undici';constgateway=newKuboCAS({gatewayBaseUrl: 'https://ipfs.my-org.dev',dispatcher: newAgent({allowH2: true,connections: 2})});

Use undici 7 for this setup: adding undici 8 as a dependency is incompatible with Node's bundled fetch dispatcher (nodejs/undici#5500). Local Kubo gateways using plain HTTP are unaffected and need no dispatcher.

Encryption and Decryption

jaxray supports transparent encryption and decryption of Zarr datasets using XChaCha20-Poly1305 authenticated encryption. This provides both confidentiality and integrity protection for your data.

Registering the Encryption Codec

Before opening encrypted datasets, register the encryption codec with your key management:

import{registerXChaCha20Poly1305Codec}from'@dclimate/jaxray';// Register with a hex-encoded 256-bit keyregisterXChaCha20Poly1305Codec({getKey: ()=>'00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff',// Optional: customize nonce generation (defaults to crypto.getRandomValues)// nonceGenerator: () => crypto.getRandomValues(new Uint8Array(24))});

Key Management Best Practices:

  • 🔑 Key Generation: Use cryptographically secure random number generators to create 256-bit (32 byte) keys
  • 🔒 Key Storage: Store keys securely using environment variables, key management services (KMS), or secure enclaves
  • 🎲 Nonce Generation: The default nonce generator uses crypto.getRandomValues() for secure randomness. Each encryption operation automatically generates a fresh nonce
  • ⚠️Never reuse nonces with the same key - the implementation handles this automatically

Opening Encrypted Datasets

Once the codec is registered, encrypted datasets can be opened transparently:

import{ZarrBackend}from'@dclimate/jaxray';// Open an encrypted Zarr storeconstdataset=awaitZarrBackend.open(encryptedStore);// Check if the dataset is encryptedconsole.log(dataset.isEncrypted);// true// Access data - decryption happens automaticallyconstvariable=dataset.getVariable('temperature');constdata=awaitvariable.compute();console.log(data.data);// Decrypted values

Encryption Detection

jaxray automatically detects encrypted datasets by inspecting the Zarr metadata for XChaCha20-Poly1305 codecs:

constdataset=awaitZarrBackend.open(store);if(dataset.isEncrypted){console.log('Dataset contains encrypted variables');// Ensure codec is registered with correct key}

How It Works

  1. Encryption: When data is written, each chunk is encrypted with a randomly generated 24-byte nonce. The nonce is prepended to the ciphertext (first 24 bytes)
  2. Decryption: When reading, the nonce is extracted from the chunk, and the data is decrypted and authenticated
  3. Authentication: XChaCha20-Poly1305 includes a 16-byte authentication tag that protects against tampering. Decryption will fail if the data has been modified or if the wrong key is used

Error Handling

If decryption fails (wrong key, corrupted data, or tampering detected), an error will be thrown:

try{constdata=awaitvariable.compute();}catch(error){if(error.message.match(/tag|auth/i)){console.error('Decryption failed: Invalid key or corrupted data');}}

Security Considerations

  • Authenticated Encryption: XChaCha20-Poly1305 provides both confidentiality and integrity
  • Unique Nonces: Each encryption operation uses a fresh random nonce
  • Extended Nonce: XChaCha20 uses 192-bit nonces, eliminating collision concerns
  • ⚠️Key Protection: The security depends entirely on keeping your encryption keys secret
  • ⚠️Metadata: Zarr metadata (array shapes, dimension names, etc.) is not encrypted

See the xchacha20poly1305 example for a complete working example.

Streaming Large Datasets

For large datasets that don't fit in memory, use streaming to process data in chunks:

// Stream large time series dataconstlargeData=newDataArray(/* ... large array ... */,{dims: ['time'],coords: {time: /* ... */}});conststream=largeData.selStream({time: ['2020-01-01','2020-12-31']},{chunkSize: 50}// Process in 50MB chunks);forawait(constchunkofstream){console.log(`Progress: ${chunk.progress}%`);console.log(`Processing ${chunk.bytesProcessed} / ${chunk.totalBytes} bytes`);// Process this chunk (e.g., write to file, compute statistics)awaitprocessChunk(chunk.data);}

Streaming also works with Datasets:

conststream=dataset.selStream({time: ['2020-01-01','2020-12-31'],lat: 45,lon: -73},{chunkSize: 100,method: 'nearest'});forawait(constchunkofstream){consttemp=chunk.data.getVariable('temperature');constpressure=chunk.data.getVariable('pressure');// Write chunk to disk or process incrementallyawaitwriteToFile(temp,pressure);}

Aggregations

// Sum all valuesconsttotal=temperatures.sum();console.log(total);// 111// Mean of all valuesconstaverage=temperatures.mean();console.log(average);// 22.2// Sum along a dimensionconstrowSums=gridData.sum('x');console.log(rowSums.data);// [6, 15]// Extremes, spread and median — whole-array or along a dimensionconsole.log(temperatures.min(),temperatures.max());console.log(temperatures.median());console.log(temperatures.std());// population std (ddof=0), as in xarrayconstrowMaxima=gridData.max('x');console.log(rowMaxima.data);// [3, 6]

mean, min, max, std and median skip masked and non-numeric values, matching xarray's skipna default — so where()-masked cells are excluded from both the value and the denominator. A slice with no numeric values reduces to NaN rather than to 0 or Infinity:

constmasked=temperatures.where(someCondition);masked.mean();// averages only the unmasked valuesmasked.max();// NaN if every value is masked

sum is the exception. It adds values with + rather than skipping non-numeric ones, and an entirely masked slice sums to 0, not NaN — so a zero from sum may mean "no data" rather than "adds up to nothing". Use count() alongside it, or check another aggregation, when that distinction matters.

median holds the values it reduces in memory in order to sort them, so it costs O(k) space in the length of the reduced dimension. The other aggregations accumulate in constant space.

Working with Datasets

import{DataArray,Dataset}from'@dclimate/jaxray';// Create multiple related DataArraysconsttemp=newDataArray([[15,16],[18,19]],{dims: ['lat','lon'],coords: {lat: [40.0,40.5],lon: [-74.0,-73.5]}});constpressure=newDataArray([[1013,1014],[1012,1013]],{dims: ['lat','lon'],coords: {lat: [40.0,40.5],lon: [-74.0,-73.5]}});// Combine into a Datasetconstweather=newDataset({temperature: temp,pressure: pressure});console.log(weather.dataVars);// ['temperature', 'pressure']console.log(weather.dims);// ['lat', 'lon']// Select from Datasetconstlocation=awaitweather.sel({lat: 40.0,lon: -73.5});consttempAtLocation=location.getVariable('temperature');console.log(tempAtLocation?.data);// 16// Works with nearest neighbor tooconstnearLocation=awaitweather.sel({lat: 40.2,lon: -73.7},{method: 'nearest'});

Merging Datasets

consthumidity=newDataArray([[65,70],[68,72]],{dims: ['lat','lon'],coords: {lat: [40.0,40.5],lon: [-74.0,-73.5]}});consthumidityData=newDataset({ humidity });constcombined=weather.merge(humidityData);console.log(combined.dataVars);// ['temperature', 'pressure', 'humidity']

Concatenating Datasets

Concatenate datasets along a dimension to combine time-series data from multiple sources. This is particularly useful for joining finalized and non-finalized data, or combining historical and recent observations:

// Create first dataset (historical data)consthistorical=newDataset({temperature: newDataArray([[15.2,16.8],[18.3,19.1],[20.5,21.2]],{dims: ['time','location'],coords: {time: ['2020','2021','2022'],location: ['Station-A','Station-B']}})});// Create second dataset (recent data)constrecent=newDataset({temperature: newDataArray([[22.1,23.4],[24.2,25.1]],{dims: ['time','location'],coords: {time: ['2023','2024'],location: ['Station-A','Station-B']}})});// Concatenate along time dimensionconstcombined=historical.concat(recent,{dim: 'time'});console.log(combined.sizes.time);// 5console.log(combined.coords.time);// ['2020', '2021', '2022', '2023', '2024']// Query across both datasetsconstdata=awaitcombined.sel({time: ['2021','2022','2023']});constcomputed=awaitdata.compute();console.log(computed.getVariable('temperature').data);// [[18.3, 19.1], [20.5, 21.2], [22.1, 23.4]]

Key Features:

  • 🔗 Lazy Evaluation: The result is a lazy dataset that only fetches data when needed
  • 🎯 Smart Routing: Automatically queries the correct source dataset(s) based on selection
  • 📊 Multi-Variable: Works with datasets containing multiple variables
  • Validation: Ensures dimensions and variables match across datasets

Use Cases:

  • Combining finalized and provisional weather data
  • Joining historical archives with recent observations
  • Merging data from different time periods or sources

See the concat-datasets example for more details.

Conditional Selection with Where

Use the where() method to filter data based on conditions. Create conditions using comparison methods like .lt(), .gt(), .le(), .ge():

constdata=newDataArray([10,20,30,40,50],{dims: ['time'],coords: {time: ['Mon','Tue','Wed','Thu','Fri']}});// Filter where values > 25 (keep values meeting condition, replace others with NaN)constcondition=data.gt(25);constfiltered=data.where(condition);console.log(filtered.data);// [NaN, NaN, 30, 40, 50]// Replace values not meeting condition with 0constreplaced=data.where(condition,0);console.log(replaced.data);// [0, 0, 30, 40, 50]// Works with datasets tooconsttemp=weather.getVariable('temperature');consthot=temp.gt(15);constfiltered_weather=weather.where(hot);// Both temperature and pressure filtered by the condition

Available comparison methods: .lt(), .le(), .gt(), .ge(), .equal(), .notEqual()

Rolling Windows

Calculate rolling statistics over a dimension:

consttemps=newDataArray([10,15,20,18,22,25,23],{dims: ['time'],coords: {time: ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']}});// Rolling mean with window size 3constrolling_mean=temps.rolling('time',3).mean();console.log(rolling_mean.data);// [NaN, NaN, 15, 17.67, 20, 21.67, 23.33]// Rolling sumconstrolling_sum=temps.rolling('time',3).sum();console.log(rolling_sum.data);// [NaN, NaN, 45, 53, 60, 65, 70]// Works with datasetsconstrolling_weather=weather.rolling('lat',2).mean();// Both temperature and pressure computed with rolling mean

Renaming Variables

Rename data variables in a dataset:

constweather=newDataset({temperature: temp,pressure: pressure});// Rename single variableconstrenamed=weather.rename({temperature: 'temp'});console.log(renamed.dataVars);// ['temp', 'pressure']// Rename multiple variablesconstrenamed_all=weather.rename({temperature: 'temp',pressure: 'pres'});console.log(renamed_all.dataVars);// ['temp', 'pres']

Assigning Coordinates

Update or add coordinates to a dataset:

constweather=newDataset({temperature: temp,pressure: pressure});// Update existing coordinatesconstupdated=weather.assignCoords({lat: [41.0,41.5],// New latitude valueslon: [-75.0,-74.5]// New longitude values});// Coordinates can also be DataArraysconstnew_lats=newDataArray([41.0,41.5],{dims: ['lat']});constupdated2=weather.assignCoords({lat: new_lats});

Dropping Variables

Remove variables from a dataset:

constweather=newDataset({temperature: temp,pressure: pressure,humidity: humidity});// Drop single variableconstdropped=weather.dropVars('humidity');console.log(dropped.dataVars);// ['temperature', 'pressure']// Drop multiple variablesconstdropped_multiple=weather.dropVars(['humidity','pressure']);console.log(dropped_multiple.dataVars);// ['temperature']

Squeezing Dimensions

Remove dimensions of size 1:

constdata=newDataArray([[[1,2,3]]],{dims: ['x','y','z'],coords: {x: [0],// size 1 - will be squeezedy: [10],// size 1 - will be squeezedz: [100,101,102]}});constsqueezed=data.squeeze();console.log(squeezed.dims);// ['z']console.log(squeezed.shape);// [3]console.log(squeezed.data);// [1, 2, 3]// Works with datasets tooconstsqueezed_weather=weather.squeeze();// All dimensions of size 1 are removed from all variables

API Reference

DataArray

Constructor

newDataArray(data,options?)

Parameters:

  • data: Multi-dimensional array of values
  • options:
    • dims: Array of dimension names
    • coords: Coordinate values for each dimension
    • attrs: Metadata attributes
    • name: Name of the DataArray

Properties

  • data: Get the underlying data
  • values: Alias for data
  • dims: Array of dimension names
  • shape: Array of dimension sizes
  • coords: Coordinate values
  • attrs: Metadata attributes
  • name: Name of the DataArray
  • ndim: Number of dimensions
  • size: Total number of elements

Methods

  • sel(selection, options?): Select by coordinate labels

    • options.method: Selection method ('nearest', 'ffill', 'bfill', 'pad', 'backfill')
    • options.tolerance: Maximum distance for method selection
  • selStream(selection, options?): Stream selection in chunks (returns AsyncGenerator)

    • options.chunkSize: Target chunk size in MB (default: 100)
    • options.dimension: Dimension to chunk along (default: auto-detect)
    • options.method: Selection method
    • options.tolerance: Maximum distance for method selection
  • isel(selection): Select by integer positions

  • where(condition, other?, options?): Filter data based on a condition

    • condition: Boolean DataArray (create with comparison methods like .lt(), .gt(), etc.)
    • other: Optional replacement value where condition is false (default: NaN)
    • options.keepAttrs: Preserve attributes in result (default: false)
    • options.skipNa: Skip NaN values in condition (default: false)
  • rolling(dim, window, options?): Create rolling window object

    • Returns DataArrayRolling with .mean() and .sum() methods
    • window: Size of rolling window
    • options.min_periods: Minimum observations in window (default: window size)
    • options.center: Center the window (default: false)
  • squeeze(): Remove dimensions of size 1

  • sum(dim?): Sum along dimension (or all values)

  • mean(dim?): Mean along dimension (or all values)

  • min(dim?): Minimum along dimension (or all values)

  • max(dim?): Maximum along dimension (or all values)

  • std(dim?): Population standard deviation, ddof=0 (or all values)

  • median(dim?): Median along dimension (or all values)

    mean, min, max, std and median skip masked/non-numeric values (xarray skipna semantics) and return NaN when nothing numeric remains. sum does not: it returns 0 for an all-masked slice.

  • toObject(): Convert to plain JavaScript object

  • toJSON(): Convert to JSON string

Dataset

Constructor

newDataset(dataVars?,options?)

Parameters:

  • dataVars: Object mapping variable names to DataArrays
  • options:
    • coords: Shared coordinates
    • attrs: Metadata attributes

Properties

  • dataVars: Array of variable names
  • dims: Array of all dimension names
  • coords: Shared coordinates
  • attrs: Metadata attributes
  • sizes: Object mapping dimension names to sizes

Methods

  • addVariable(name, dataArray): Add a new variable
  • getVariable(name): Get a variable by name (throws if not found)
  • get(key): Dictionary-style access - get('varname') or get(['var1', 'var2'])
  • hasVariable(name): Check if variable exists
  • removeVariable(name): Remove a variable
  • sel(selection, options?): Select by coordinate labels
    • options.method: Selection method ('nearest', 'ffill', 'bfill', 'pad', 'backfill')
    • options.tolerance: Maximum distance for method selection
  • selStream(selection, options?): Stream selection in chunks (returns AsyncGenerator)
    • options.chunkSize: Target chunk size in MB (default: 100)
    • options.dimension: Dimension to chunk along (default: auto-detect)
    • options.method: Selection method
    • options.tolerance: Maximum distance for method selection
  • isel(selection): Select by integer positions
  • map(fn): Apply function to all variables
  • where(condition, other?, options?): Filter data based on a condition
    • condition: Boolean DataArray or Dataset (create with comparison methods like .lt(), .gt(), etc.)
    • other: Optional replacement value where condition is false (default: NaN)
    • options.keepAttrs: Preserve attributes in result (default: false)
    • options.skipNa: Skip NaN values in condition (default: false)
  • rolling(dim, window, options?): Create rolling window object
    • Returns DatasetRolling with .mean() and .sum() methods
    • window: Size of rolling window
    • options.min_periods: Minimum observations in window (default: window size)
    • options.center: Center the window (default: false)
  • rename(mapping): Rename variables
    • mapping: Object mapping old names to new names
  • assignCoords(coords): Update or add coordinates
    • coords: Object mapping dimension names to coordinate values or DataArrays
  • dropVars(names): Remove variables
    • names: String or array of variable names to drop
  • squeeze(): Remove dimensions of size 1
  • merge(other): Merge with another Dataset
  • toObject(): Convert to plain JavaScript object
  • toJSON(): Convert to JSON string

Examples

See the examples directory for more detailed usage examples.

Development

# Install dependencies
npm install
# Build
npm run build
# Run tests
npm test

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Inspiration

This library is inspired by Python's xarray library, which provides labeled multi-dimensional arrays for scientific computing.

About

A javascript implementation similar to xarray

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages