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

groupjs_by

v2.0.5

Published

Zero-dependency groupBy and aggregates for arrays of objects (sum, avg, min, max, count)

Readme

groupjs_by

npm version npm downloads CI License: MIT TypeScript

Zero-dependency JavaScript library for grouping arrays of objects and computing aggregates — with a chainable, SQL-inspired API.

const { groupBy } = require('groupjs_by');

groupBy(orders, 'status')
  .sum('revenue', 'amount')
  .avg('avgOrder', 'amount')
  .count('orders')
  .data;

Why groupjs_by?

| Need | How groupjs_by helps | |------|----------------------| | Group + aggregate in one place | Chain groupBy → aggregates → .data | | Multiple dimensions | Pass ['country', 'status'] (or accessors) | | Chart-friendly rows | Call .toArray() for [{ key, items, …aggs }] | | Sorted tables | .orderBy('revenue', 'desc') | | Custom metrics | .reduce(alias, fn, initial) | | Large payloads | .omitItems() after aggregating | | Multiple metrics efficiently | aggregate() scans each group once | | Nested or derived keys | Pass (item) => … accessors anywhere a column is expected | | Typed consumers | Ships with TypeScript declarations | | Small surface area | No transitive dependencies |

Built for reporting, dashboards, ETL transforms, and any pipeline that looks like GROUP BY + SUM / AVG / MIN / MAX / COUNT.


Install

npm install groupjs_by
# or
yarn add groupjs_by

CommonJS

const { groupBy } = require('groupjs_by');

ESM

import { groupBy } from 'groupjs_by';
// or
import groupjs from 'groupjs_by';

Works in Node.js 18+ and any bundler. TypeScript types are included for both require and import.

See CHANGELOG.md for release history.


Quick start

const { groupBy } = require('groupjs_by');

const orders = [
  { status: 'paid', amount: 120, sku: 'TEE-BLK' },
  { status: 'paid', amount: 80, sku: 'HAT-RED' },
  { status: 'refunded', amount: 40, sku: 'TEE-BLK' },
];

const byStatus = groupBy(orders, 'status')
  .sum('revenue', 'amount')
  .avg('avgOrder', 'amount')
  .count('orders')
  .data;

Result

{
  paid: {
    items: [/* … */],
    revenue: 200,
    avgOrder: 100,
    orders: 2,
  },
  refunded: {
    items: [/* … */],
    revenue: 40,
    avgOrder: 40,
    orders: 1,
  },
}

Recipes

Copy-paste examples for common reporting jobs.

Sales report by status

Revenue, average order value, and order count — useful for checkout dashboards.

const { groupBy } = require('groupjs_by');

const orders = [
  { id: 'o1', status: 'paid', amount: 120.5, channel: 'web' },
  { id: 'o2', status: 'paid', amount: 89.0, channel: 'app' },
  { id: 'o3', status: 'pending', amount: 45.0, channel: 'web' },
  { id: 'o4', status: 'refunded', amount: 30.0, channel: 'web' },
  { id: 'o5', status: 'paid', amount: 210.0, channel: 'retail' },
];

// Paid / pending / refunded summary
const salesByStatus = groupBy(orders, 'status').aggregate({
  sum: ['revenue', 'amount'],
  avg: ['avgOrderValue', 'amount', 2],
  min: ['smallestOrder', 'amount'],
  max: ['largestOrder', 'amount'],
  count: ['orderCount'],
}).data;

console.log(salesByStatus.paid.revenue); // 419.5
console.log(salesByStatus.paid.orderCount); // 3

// Only completed revenue (filter first)
const paidOnly = groupBy(orders, 'status')
  .where((order) => order.status === 'paid')
  .aggregate({
    sum: ['revenue', 'amount'],
    distinctCount: ['channels', 'channel'],
    count: ['orderCount'],
  }).data;

console.log(paidOnly.paid);
// { items: […], revenue: 419.5, channels: 3, orderCount: 3 }

Sales by channel (nested fields)

When metrics live under a nested object, use accessors:

const checkoutEvents = [
  { meta: { channel: 'web' }, payment: { total: 50 } },
  { meta: { channel: 'web' }, payment: { total: 75 } },
  { meta: { channel: 'app' }, payment: { total: 120 } },
];

const byChannel = groupBy(checkoutEvents, (row) => row.meta.channel).aggregate({
  sum: ['revenue', (row) => row.payment.total],
  avg: ['avgTicket', (row) => row.payment.total],
  count: ['checkouts'],
}).data;

console.log(byChannel.web.revenue); // 125
console.log(byChannel.app.checkouts); // 1

Request / error logs by day

Roll up API or app logs for daily volume and error rates.

const { groupBy } = require('groupjs_by');

const logs = [
  { ts: '2026-03-01T08:12:00Z', level: 'info', route: '/api/orders', latencyMs: 42 },
  { ts: '2026-03-01T09:04:00Z', level: 'error', route: '/api/orders', latencyMs: 310 },
  { ts: '2026-03-01T18:22:00Z', level: 'info', route: '/api/cart', latencyMs: 28 },
  { ts: '2026-03-02T10:01:00Z', level: 'error', route: '/api/checkout', latencyMs: 900 },
  { ts: '2026-03-02T11:45:00Z', level: 'warn', route: '/api/orders', latencyMs: 120 },
  { ts: '2026-03-02T15:10:00Z', level: 'info', route: '/api/orders', latencyMs: 35 },
];

// Group by calendar day (UTC)
const dayKey = (log) => log.ts.slice(0, 10);

const logsByDay = groupBy(logs, dayKey).aggregate({
  count: ['events'],
  avg: ['avgLatencyMs', 'latencyMs', 1],
  max: ['pWorstLatencyMs', 'latencyMs'],
  distinctCount: ['routesHit', 'route'],
}).data;

console.log(logsByDay['2026-03-01'].events); // 3
console.log(logsByDay['2026-03-02'].avgLatencyMs); // 351.7

// Errors only, still keyed by day
const errorsByDay = groupBy(logs, dayKey)
  .where((log) => log.level === 'error')
  .aggregate({
    count: ['errors'],
    avg: ['avgErrorLatencyMs', 'latencyMs'],
    distinctCount: ['failingRoutes', 'route'],
  }).data;

console.log(errorsByDay['2026-03-01'].errors); // 1
console.log(errorsByDay['2026-03-02'].failingRoutes); // 1

Inventory by SKU

Stock levels, warehouse spread, and movement totals for ops / replenishment views.

const { groupBy } = require('groupjs_by');

const movements = [
  { sku: 'TEE-BLK', warehouse: 'US-EAST', qty: 40, unitCost: 8 },
  { sku: 'TEE-BLK', warehouse: 'US-WEST', qty: 12, unitCost: 8 },
  { sku: 'TEE-BLK', warehouse: 'US-EAST', qty: -5, unitCost: 8 }, // outbound
  { sku: 'HAT-RED', warehouse: 'US-EAST', qty: 20, unitCost: 15 },
  { sku: 'HAT-RED', warehouse: 'EU-CENTRAL', qty: 8, unitCost: 15 },
  { sku: 'MUG-WHT', warehouse: 'US-WEST', qty: 100, unitCost: 4 },
];

const inventoryBySku = groupBy(movements, 'sku').aggregate({
  sum: ['onHand', 'qty'],
  avg: ['avgUnitCost', 'unitCost'],
  distinctCount: ['warehouses', 'warehouse'],
  count: ['ledgerLines'],
}).data;

console.log(inventoryBySku['TEE-BLK'].onHand); // 47  (40 + 12 - 5)
console.log(inventoryBySku['TEE-BLK'].warehouses); // 2
console.log(inventoryBySku['HAT-RED'].onHand); // 28

// Low-stock SKUs only (after aggregating, filter keys you care about)
const lowStock = Object.entries(inventoryBySku)
  .filter(([, row]) => row.onHand < 50)
  .map(([sku, row]) => ({ sku, onHand: row.onHand, warehouses: row.warehouses }));

console.log(lowStock);
// [
//   { sku: 'TEE-BLK', onHand: 47, warehouses: 2 },
//   { sku: 'HAT-RED', onHand: 28, warehouses: 2 },
// ]

Inventory valuation by warehouse

Derive line value with an accessor, then sum it per warehouse:

const lineValue = (row) => row.qty * row.unitCost;

const valuation = groupBy(movements, 'warehouse')
  .where((row) => row.qty > 0) // ignore outbound for on-hand value
  .sum('units', 'qty')
  .sum('inventoryValue', lineValue)
  .distinctCount('skus', 'sku')
  .count('lines')
  .data;

console.log(valuation['US-EAST'].inventoryValue); // 40*8 + 20*15 = 620
console.log(valuation['US-WEST'].skus); // 2

Multi-key groupBy (country × status)

Pass an array of fields (or accessors) to group on more than one dimension:

const { groupBy } = require('groupjs_by');

const orders = [
  { country: 'US', status: 'paid', amount: 100 },
  { country: 'US', status: 'paid', amount: 50 },
  { country: 'US', status: 'refunded', amount: 20 },
  { country: 'MX', status: 'paid', amount: 80 },
];

const byCountryStatus = groupBy(orders, ['country', 'status'])
  .sum('revenue', 'amount')
  .count('orders');

// `.keys` returns composite key arrays
console.log(byCountryStatus.keys);
// [['US', 'paid'], ['US', 'refunded'], ['MX', 'paid']]

// `.data` uses JSON-stringified composite keys as object keys
console.log(byCountryStatus.data[JSON.stringify(['US', 'paid'])].revenue); // 150

Export rows with .toArray()

Prefer .toArray() for tables, charts, and CSV — each group becomes one object with a key field:

const rows = groupBy(orders, ['country', 'status'])
  .sum('revenue', 'amount')
  .count('orders')
  .toArray();

console.log(rows);
// [
//   { key: ['US', 'paid'], items: […], revenue: 150, orders: 2 },
//   { key: ['US', 'refunded'], items: […], revenue: 20, orders: 1 },
//   { key: ['MX', 'paid'], items: […], revenue: 80, orders: 1 },
// ]

// Single-key groups use a string `key`
const byStatus = groupBy(orders, 'status').count('n').toArray();
// [{ key: 'paid', items: […], n: 3 }, { key: 'refunded', items: […], n: 1 }]

Sort, custom metrics, and slim output

const leaderboard = groupBy(orders, 'status')
  .sum('revenue', 'amount')
  .reduce(
    'skus',
    (acc, order) => {
      acc.push(order.sku);
      return acc;
    },
    []
  )
  .orderBy('revenue', 'desc')
  .omitItems() // drop raw rows — call this last
  .toArray();

// [
//   { key: 'paid', revenue: 150, skus: ['A', 'B'] },
//   { key: 'pending', revenue: 80, skus: ['A'] },
//   …
// ]

Cookbook

Single-pass multi-aggregate

Prefer aggregate() when you need several metrics — one scan per group instead of one scan per chained method.

groupBy(orders, 'status').aggregate({
  sum: ['revenue', 'amount'],
  avg: ['avgOrder', 'amount'],
  min: ['smallest', 'amount'],
  max: ['largest', 'amount'],
  distinctCount: ['skus', 'sku'],
  count: ['orders'],
}).data;

| Spec key | Entry | Notes | |------------------|------------------------------------|--------------------------------| | sum | [alias, column] | Numeric sum | | avg | [alias, column, decimals?] | Defaults to 2 decimal places | | min / max | [alias, column] | Empty groups → null | | distinctCount | [alias, column] | Unique value count | | count | [alias] | Item count (no column) |

Filter before aggregating

groupBy(orders, 'status')
  .where((order) => order.amount >= 100)
  .sum('revenue', 'amount')
  .count('orders')
  .data;

Groups that become empty after where are removed. keys, firstGroup, and lastGroup stay in sync.

Nested fields & accessors

Every column argument accepts a string key or a function:

groupBy(checkoutEvents, (row) => row.meta.channel)
  .aggregate({
    sum: ['revenue', (row) => row.payment.total],
    count: ['checkouts'],
  })
  .data;

Multi-key accessors work the same way:

groupBy(rows, [
  (row) => row.meta.region,
  (row) => row.meta.plan,
]).sum('totalSeats', 'seats').toArray();

TypeScript

import groupjs = require('groupjs_by');

interface Order {
  status: string;
  amount: number;
}

const result = groupjs.groupBy(orders as Order[], 'status')
  .sum('revenue', 'amount')
  .count('orders')
  .data;

API

groupBy(data, key)

| Param | Type | Description | |--------|------|-------------| | data | T[] | Non-empty array of objects | | key | string \| (item) => any \| Array<string \| (item) => any> | Single field, accessor, or multi-key list |

Returns a chainable GroupResult. Throws if data is empty / not an array, or if a string key is missing on any object.

For multi-key grouping, .data stores groups under JSON.stringify(keyParts). Prefer .keys or .toArray() when consuming composite keys.


Aggregates

All aggregate methods (except count) take an alias (output property name) and a column (string or accessor). They return this for chaining.

| Method | Signature | Empty group | |--------|-----------|-------------| | .sum | (alias, column) | 0 | | .avg | (alias, column, decimals = 2) | null | | .min | (alias, column) | null | | .max | (alias, column) | null | | .distinctCount | (alias, column) | 0 | | .count | (alias) | 0 | | .aggregate | (spec) | Same rules per metric | | .reduce | (alias, reducer, initial?) | Same as Array#reduce |

.aggregate({
  sum: ['total', 'amount'],
  avg: ['mean', 'amount', 2],
  min: ['lo', 'amount'],
  max: ['hi', 'amount'],
  distinctCount: ['skus', 'sku'],
  count: ['n'],
})

// Custom per-group fold (array/object initials are shallow-cloned per group)
.reduce('skus', (acc, row) => { acc.push(row.sku); return acc; }, [])
.reduce('product', (acc, row) => acc * row.amount, 1)

.orderBy(field, direction = 'asc')

Reorder groups (updates .data key order, .keys, and .toArray()).

| Param | Type | Description | |-------|------|-------------| | field | 'key' \| string \| (group, key) => any | Sort by group key, an aggregate alias, or a custom value | | direction | 'asc' \| 'desc' | Default 'asc'; nullish values sort last on asc |

.orderBy('revenue', 'desc')
.orderBy('key', 'asc')
.orderBy((group) => group.n, 'desc')

.omitItems()

Deletes items from every group to free memory. Call after aggregations / where / reduce. Further item-based ops throw.


.where(predicate)

| Param | Type | Description | |-------------|----------------------|-------------------------| | predicate | (item: T) => boolean | Keep item when truthy |

Filters items inside each group. Empty groups are deleted from .data.


.toArray()

Returns an array of group rows (insertion order):

[{ key, items, ...aliases }]
  • Single-key: key is a string (same as Object.keys on .data)
  • Multi-key: key is an array of part values, e.g. ['US', 'paid']

Useful for sorting, mapping to UI tables, or serializing without composite object keys.


Result properties

| Property | Type | Description | |---------------|------------|-------------| | .data | object | { [groupKey]: { items, …aliases } } | | .keys | any[] | Current group keys (strings, or arrays when multi-key) | | .firstGroup | T[] | Items in the first group | | .lastGroup | T[] | Items in the last group |

keys, firstGroup, and lastGroup reflect the latest state after where.


Performance notes

  • min / max use tight loops (safe on groups with 100k+ rows — no argument-spread stack limits).
  • distinctCount uses a Set (linear in group size).
  • aggregate() is the fastest path when computing several metrics together.

Local micro-benchmark (optional, not a CI gate):

npm run bench

Compatibility

  • Runtime: Node.js 18+ (CommonJS + ESM); browsers via bundlers
  • Entry points: require('groupjs_by')index.js; importindex.mjs
  • Dependencies: none
  • Types: bundled (index.d.ts / index.d.mts)

Contributing

git clone https://github.com/juli04guilar/groupBy.git
cd groupBy
yarn install   # or npm install
npm test

Pull requests and issues are welcome.


License

MIT © Julio Aguilar