npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dimgrid

v0.2.3

Published

Build a typed N-dimensional grid of objects by adding named dimensions with discrete values

Readme

dimgrid

npm version CI Coverage Bundle size

Changelog

Build a typed N-dimensional grid of objects by adding named dimensions with discrete values.

Start from a single empty point and expand it into a full cartesian product by adding dimensions one at a time. Points are generated lazily — each .dim() call stores dimension metadata, and the cartesian product is only computed when you iterate or call .toArray().

Install

npm install dimgrid

Usage

import { dimgrid } from 'dimgrid'

const points = dimgrid()
  .dim('color', ['red', 'green', 'blue'])
  .dim('size', ['S', 'M', 'L'])
  .toArray()

// 9 points — every combination of color × size
// [
//   { color: 'red',   size: 'S' },
//   { color: 'red',   size: 'M' },
//   { color: 'red',   size: 'L' },
//   { color: 'green', size: 'S' },
//   ...
// ]

TypeScript infers the full type of each point from the chain, so points is typed as { color: 'red' | 'green' | 'blue'; size: 'S' | 'M' | 'L' }[].

Dynamic dimension values

Pass a function instead of an array to derive values from the point being expanded. The function receives the current point and returns the values for the new dimension. Return an empty array to drop a point entirely.

const points = dimgrid()
  .dim('sign', [-1, 1])
  .dim('magnitude', ({ sign }) => sign > 0 ? [1, 2, 3] : [1])
  .toArray()

// [
//   { sign: -1, magnitude: 1 },
//   { sign:  1, magnitude: 1 },
//   { sign:  1, magnitude: 2 },
//   { sign:  1, magnitude: 3 },
// ]

API

dimgrid()

Creates a new grid with a single empty point. All grids start here.

grid.dim(key, values)

Expands every existing point across the given values. Returns a new DimGrid — the original is not mutated.

| Parameter | Type | Description | |-----------|------|-------------| | key | string | Dimension name, becomes a property on each point | | values | readonly V[] | One child point per value | | values | (point: T) => readonly V[] | Values derived from the parent point; return [] to drop it |

grid.toArray()

Returns all points as a plain T[].

grid.size

Number of points in the grid. For grids with only static value arrays this is computed in O(dimensions) by multiplying dimension lengths — no iteration needed. For grids that use a function dimension, iteration is required and the cost is O(points).

grid[Symbol.iterator]

The grid is directly iterable — for...of and spread both work.

for (const point of grid) { ... }
const points = [...grid]

Examples

Vitest — test.each with all dimension permutations

test.each accepts an array of objects and feeds each one as named arguments to the test function — a natural fit for dimgrid points. The dimension chain replaces manual case lists that grow stale as requirements change.

The example below tests a clamp(value, min, max) utility across all combinations of inputs and bounds. The function form of .dim() computes the expected result directly from each point's other dimensions, so no separate lookup table is needed and the expected value is always in sync with the inputs.

import { describe, expect, test } from 'vitest'
import { dimgrid } from 'dimgrid'
import { clamp } from './clamp'

const cases = dimgrid()
  .dim('value',    [-20, 0, 10, 50])  // below range, at min, inside, above max
  .dim('min',      [0, 5])
  .dim('max',      [10, 30])
  .dim('expected', ({ value, min, max }) => [
    value < min ? min : value > max ? max : value,
  ])
  .toArray()

describe('clamp', () => {
  test.each(cases)(
    'clamp($value, $min, $max) → $expected',
    ({ value, min, max, expected }) => {
      expect(clamp(value, min, max)).toBe(expected)
    },
  )
})

This produces 4 × 2 × 2 = 16 test cases automatically, with names like:

clamp(-20, 0, 10) → 0
clamp(-20, 0, 30) → 0
clamp(-20, 5, 10) → 5
clamp(10, 0, 10)  → 10
clamp(50, 0, 10)  → 10
...

Adding a new boundary value to any dimension (say, max: [10, 20, 30]) inserts a full slice of tests with no further changes, keeping coverage complete across all combinations.

Storybook — visual matrix of all component states

Design systems need stories for every meaningful prop combination. Writing them by hand is tedious and incomplete; dimgrid generates the full matrix and the function form prunes states that are visually invalid or redundant before they reach the story.

The example below covers a Button with four dimensions. A button cannot be both disabled and loading at the same time, so loading uses the function form to restrict itself to [false] whenever disabled is true.

// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { dimgrid } from 'dimgrid'
import { Button } from './Button'

const meta: Meta<typeof Button> = { component: Button }
export default meta

const cases = dimgrid()
  .dim('variant',  ['primary', 'secondary', 'ghost', 'danger'])
  .dim('size',     ['sm', 'md', 'lg'])
  .dim('disabled', [false, true])
  .dim('loading',  ({ disabled }) => disabled ? [false] : [false, true])
  .toArray()
// 4 × 3 × 2 × 2 = 48 raw combinations, pruned to 36 by the loading constraint

export const AllVariants: StoryObj<typeof Button> = {
  render: () => (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
      {cases.map((props, i) => <Button key={i} {...props} />)}
    </div>
  ),
}

A single AllVariants story renders all 36 states in one snapshot. Visual regression tools like Chromatic catch regressions across the entire matrix on every commit. Adding a new variant value to the first dimension automatically propagates across all size × disabled × loading combinations with no other changes.

ML hyperparameter grid search

Grid search — systematically training a model for every combination of hyperparameters and picking the best result — is where the name "grid" comes from. dimgrid generates the search space; the function form prunes configurations that are known to be numerically unstable before any training job is launched.

SGD diverges at high learning rates, so the optimizer dimension restricts itself to ['adam'] whenever learningRate exceeds 1e-3:

// grid-search.ts
import { dimgrid } from 'dimgrid'

const configs = dimgrid()
  .dim('learningRate', [1e-4, 1e-3, 1e-2])
  .dim('batchSize',    [32, 64, 128])
  .dim('dropout',      [0.0, 0.3, 0.5])
  .dim('optimizer',    ({ learningRate }) =>
    learningRate >= 1e-2 ? ['adam'] : ['adam', 'sgd']
  )
  .toArray()
// 3 × 3 × 3 × 2 = 54 raw combinations, pruned to 45

const results = await Promise.all(
  configs.map(params =>
    trainModel(params).then(({ valAccuracy, valLoss }) => ({
      ...params,
      valAccuracy,
      valLoss,
    }))
  )
)

const best = results.sort((a, b) => b.valAccuracy - a.valAccuracy)[0]
console.log('best config:', best)

trainModel is whatever launches a training run in your stack — a TensorFlow.js fit call, a Python subprocess, a remote job submitted to a GPU cluster. The dimgrid part is the same regardless.

The pruning matters at scale: a full 4-dimensional sweep without constraints wastes GPU hours on configurations that are guaranteed to fail. Adding a fifth dimension (say, weightDecay) multiplies the search space, but the function form keeps the invalid slice removed automatically.


Credits

Thanks to Santiago Arévalo for encouraging me to publish this as a library.