ndarray's view is four numbers over a flat typed array, and a subarray write lands in the parent
📈 Multidimensional arrays for JavaScript
At a glance
- What is it?
- scijs/ndarray is a small MIT licensed JavaScript library that turns any flat typed array into a strided n-dimensional view, and the whole API rests on four fields: data, shape, stride and offset. The design is deliberate and fast, and it also means a view shares its parent's memory with no copy step anywhere in the visible API.
- Who is it for?
- Take ndarray if you already hold a typed array and need shape, stride or offset arithmetic on it, especially for images, audio, matrices and other numeric work in node.js.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 57 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Star history
- scijs/ndarray1,246 ★ · +3
| Repository | 2026-08-26 | 2026-10-22 |
|---|---|---|
| scijs/ndarray | 1.2k | 1.2k |
3 Stars in the last 57 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
The view is four numbers over whatever flat store you hand it
The constructor is `ndarray(data[, shape, stride, offset])`, and every array it returns carries exactly four pieces of state: `array.data`, the flat 1D storage; `array.shape`; `array.stride`, the per-axis step in elements; and `array.offset`, where the view starts. Defaults are `shape` set to `data.length`, `stride` set to row major, and `offset` set to 0. The storage argument is loose by design: an `Array`, any typed array, or an object that implements `get()`, `set()` and `.length` all qualify.
Because stride is a separate field from shape, one structure covers row major and column major storage without a second data type. Element access is arithmetic on those four fields rather than a tree of dispatch, which is the whole reason a transposed or shifted view costs a new stride array and nothing more.
function get(i,j,...) {
return this.data[this.offset + this.stride[0] * i + this.stride[1] * j + ... ]
}`array.index(i,j, ...)` evaluates that same expression without dereferencing it, which is what you hand to a raw store when the accessor overhead is the part you want to drop.
`lo` and `hi` hand back views, so a subarray write lands in the parent
`array.lo(i,j,k,...)` shifts a view inward from the top-left corner by that amount, described as dragging the corner in by `(i,j,k...)`. `array.hi(i,j,k,...)` is the dual. Neither copies. The 5x5 example is the clearest statement of what that costs a reader:
var x = ndarray(new Float32Array(25), [5, 5])
var y = x.hi(4,4).lo(1,1)
for(var i=0; i<y.shape[0]; ++i) {
for(var j=0; j<y.shape[1]; ++j) {
y.set(i,j,1)
}
}The loop never mentions `x`, yet the comment that follows shows `x` with a 3x3 block of ones in the middle. `y` is a window onto `x.data` at an offset, so `y.set` is `x`'s memory. That is the point of the design and also the thing to watch: there is no copy on write, no ownership flag, and no method in the visible API that materialises a standalone array.
The intro promises transpose and flip, and the Slicing section stops inside `hi`
The opening claims that ndarrays can be transposed, flipped, sheared and sliced in constant time per operation. The reference that follows gets as far as Slicing, defines `array.lo(i,j,k,...)` in a full sentence, and then gives `array.hi(i,j,k,...)` a single clause: `Instead of shifting from the top-left, it t`. The file ends there, mid clause.
So the four operations named in the intro are exactly the ones a reader cannot look up. Transpose, flip, step, rank, pick, sort and the higher order variants have no entry in the visible reference at all. The same paragraph admits the constant time claim is really `O(shape.length)`, hedged by a note that `shape.length` stays under 4, which is cheap for one to three dimensions and grows with the rank you ask for.
The framing itself is also odd: constant time is claimed, then immediately redefined as a function of rank, and then excused by a guess about typical rank. So anyone planning around the four operation promise is planning around a surface the file does not describe and a cost model it declines to state.
`order` is a sorted axis list, not the memory order `stride` defaults to row major
Two members of the view sound like memory layout and are not the same value. The constructor's `stride` parameter defaults to row major, and `stride` is the per-axis step in elements. `array.order` is separate. It returns the order of the stride of the array, sorted in ascending length, where the first element is the index of the shortest stride and the last is the index of the longest. That makes it a permutation of axis indices arranged by how fast each axis moves, so a 2x2 array with row major strides of `[2, 1]` reads back as `[1, 0]`.
The remaining two properties are plain counts. `array.size` gives the number of logical elements and `array.dimension` gives the rank. All four of these, along with `dtype`, are created with `Object.defineProperty` and take no physical memory of their own.
The naming is the part that trips people up. Two members mean layout, one of them is a permutation rather than the strides themselves, and the row major default lives on a constructor argument rather than on either member. Anyone reaching for `array.order` expecting a flag to query or set will get a list of axis indices instead.
Every storage type has a name, and anything unrecognised falls through to `generic`
`array.dtype` returns a string for the underlying store, chosen from a fixed table: `Int8Array` to `int8`, `Int16Array` to `int16`, `Int32Array` to `int32`, `Uint8Array` to `uint8`, `Uint16Array` to `uint16`, `Uint32Array` to `uint32`, `BigInt64Array` to `bigint64`, `BigUint64Array` to `biguint64`, `Float16Array` to `float16`, `Float32Array` to `float32`, `Float64Array` to `float64`, `Array` to `array`, `Buffer` to `buffer`, and anything else to `generic`.
Two details in that table matter in practice. The row written `Uint8ArrayClamped` puts the word `Array` before `Clamped`, unlike every neighbour that spells the constructor with `Clamped` last, so a lookup keyed on the constructor name will not find it. And the generic path loses the array accessors: a generic store is read and written through `get()` and `set()` on the 1D object instead, so code that reaches for `array.data[i]` stops working the moment you pass a duck typed storage object.
package.json points at mikolalysenko, and the browser path is a bundler
The manifest names `Mikola Lysenko` as author and its repository field is `git://github.com/mikolalysenko/ndarray.git`, while the project now sits under the `scijs` organisation. The same split shows in the build badges at the top of the file, which resolve against `ci.testling.com/mikolalysenko/ndarray` and `travis-ci.org/mikolalysenko/ndarray`. The manifest also carries no `homepage` key and no `bugs` key, so the npm page offers no docs link and no issue tracker.
Nothing in the tree is broken by the account mismatch, but it does mean the clone URL a user copies and the repository they land in are not the same place.
The runtime surface is small: two dependencies, `iota-array` at `^1.0.0` and `is-buffer` at `^1.0.2`, the latter being why `Buffer` has its own row in the dtype table. `main` is `ndarray.js` and it is CommonJS, so there is no standalone global build. For a browser you need a bundler; the intro names browserify as the direct route and beefy for live reloading while debugging.
Version 1.1.1, no tagged release, and a browser matrix that starts at IE 10
`package.json` reports version `1.1.1` and the tree carries a `CHANGELOG.md`, but the repository has no GitHub releases, so the npm version is the only version marker a consumer can see. The test script is `tape test/*.js` with `tape` pinned at `^2.12.3`, alongside three helper packages for the view arithmetic: `permutation-rank`, `dup` and `invert-permutation`, all at `^1.0.0`. A `.npmignore` sits next to `.gitignore`, and the manifest declares no `files` array, so what ships in the tarball is decided by that hand maintained exclusion list rather than by an explicit allow list.
The browser matrix in the manifest is the oldest signal in the file: `ie/10..latest`, `firefox/17..latest`, `chrome/22..latest`, `opera/12..latest`, `safari/6.0..latest`, `android-browser/4.2..latest`, `ipad/6.0..latest`, `iphone/6.0..latest`. The default branch is `master`, and the last push to it is dated 2026-08-26.
Editorial conclusion
Take ndarray if you already hold a typed array and need shape, stride or offset arithmetic on it, especially for images, audio, matrices and other numeric work in node.js. Weigh three things first: the views are not owned, so any code path that assumes a subarray is a copy will corrupt its parent; the package has no releases, so the npm version 1.1.1 is the only version marker available; and the reference stops partway through the Slicing section, so check which of the operations named in the intro are actually documented before committing to the four field model.
Frequently asked questions
What is ndarray in JavaScript?
It is a small MIT licensed library for multidimensional arrays whose objects are views over flat 1D storage. Every view carries four fields: data, shape, stride and offset, and the shipped manifest reports version 1.1.1.
How do you install and use ndarray?
Run `npm install ndarray` and then `require("ndarray")`. The constructor is `ndarray(data[, shape, stride, offset])`, taking a typed array, an Array, or an object with get(), set() and .length, plus an optional shape array. It runs in node.js, and in a browser through a bundler such as browserify.
How do you read and write elements of an ndarray?
Use `array.get(i,j,...)` and `array.set(i,j,...,v)`. Both work out a flat position from `offset` and `stride`, so a write through a subarray view reaches the parent buffer. `array.index(i,j, ...)` returns that flat position without dereferencing it.
What is the difference between ndarray and a plain array?
A plain array is one dimensional. An ndarray adds shape, stride and offset on top of flat storage, and because stride is stored separately from shape the same structure covers both row major and column major layouts, and shifted or transposed views are expressed without copying the elements underneath.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/scijs-ndarray)Star history chart for your README
[](https://hysenlabs.com/projects/scijs-ndarray)