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

@zakkster/lite-axis

v1.0.0

Published

Zero-GC, high-performance tick generation and label formatting for charts. Produces linear, logarithmic, and time-scale ticks without hot-path heap allocations.

Downloads

70

Readme

@zakkster/lite-axis

npm version npm bundle size npm downloads npm total downloads TypeScript Dependencies License: MIT

📈 What is lite-axis?

@zakkster/lite-axis is a zero-GC tick-generator and label-formatter toolkit for charts. Given a numeric domain [min, max] and a target tick count, it produces the "nice" tick values humans expect — 1, 2, 5, 10, 25, 50, 100, ... — without ever allocating on the hot path.

It gives you:

  • 📏 3 tick generators — linear (1/2/5 family), log (decade + 2×/5×), and UTC time (ms → year)
  • 🔤 3 label formatters — numbers, currency, and time → ASCII char codes
  • ✂️ Collision-aware label thinning — culls overlapping labels by pixel distance
  • 0️⃣ Zero allocations on the hot path — caller owns every buffer
  • 🎯 Char-code output — drop straight into @zakkster/lite-bmfont's drawFast API
  • 🛡️ Strictly UTC — no Intl.DateTimeFormat, no DST surprises, no locale lookups
  • 🪶 Tiny — no dependencies, ESM, fully tree-shakeable

Part of the @zakkster/lite-* ecosystem — micro-libraries built for deterministic, cache-friendly game and chart rendering.

🚀 Install

npm i @zakkster/lite-axis

⚡ Quick Start

import {
    linearTicks, logTicks, timeTicks,
    formatNumber, formatCurrency, formatTime,
    thinLabels,
    TIME_UNIT
} from '@zakkster/lite-axis';

// 1. Tick values land in a Float64Array you own
const ticks = new Float64Array(20);
const n = linearTicks(12, 87, 6, ticks);
// ticks ≈ [0, 20, 40, 60, 80, 100], n = 6

// 2. Format them into a Uint8Array of ASCII char codes
const buf = new Uint8Array(16);
const written = formatNumber(ticks[1], 0, buf, 0);
// buf[0..written] = "20"

// 3. Time axes pick the right unit for you
const timeOut = new Float64Array(20);
const res = timeTicks(Date.UTC(2024, 0, 1), Date.UTC(2024, 6, 1), 6, timeOut);
// res.unit === TIME_UNIT.MONTH, res.count === 7

// 4. Thin overlapping labels by pixel distance
const px = new Float64Array(20);
const idx = new Int32Array(20);
for (let i = 0; i < n; i++) px[i] = (ticks[i] / 100) * canvasWidth;
const kept = thinLabels(ticks, n, px, /* minPx */ 50, idx);
// idx[0..kept] holds indices of the labels to actually render

Every function takes pre-allocated buffers and returns the number of items written. No allocations. No GC pressure. No reentrancy bugs.

📊 Comparison

| Library | Size | Allocations / call | Tick types | Format | |---------|------|--------------------|------------|--------| | d3-scale + d3-time-format | ~30 KB | many | linear / log / time | ESM | | chartjs-adapter-date-fns | ~80 KB | many | time only | ESM (peer dep) | | lite-axis | ~3 KB | 0 (caller-owned buffers) | linear / log / time | ESM, tree-shakeable |

⚙️ API

Tick Generators

linearTicks(min, max, target, out, mode?)

Generate tick values from the 1/2/5/10 family.

const out = new Float64Array(20);
const n = linearTicks(12, 87, 5, out, 'extend');
// out = [0, 20, 40, 60, 80, 100], n = 6 — rounds outward (default)

const n2 = linearTicks(12, 87, 5, out, 'inside');
// out = [20, 40, 60, 80], n2 = 4 — strictly within [min, max]
  • 'extend' (default) — rounds outward; ticks may exceed [min, max]. Best for grid lines.
  • 'inside' — bounds ticks strictly within [min, max]. Best when ticks are axis-bounded.
  • Reversed bounds (max < min) are swapped internally.
  • Floating-point artifacts are snapped (0.300000000000000040.3).

logTicks(min, max, target, out, minor?)

Generate base-10 logarithmic ticks.

const out = new Float64Array(20);
const n = logTicks(0.5, 1500, 5, out);
// out = [1, 10, 100, 1000], n = 4

const n2 = logTicks(10, 100, 10, out, /* minor */ true);
// out = [10, 20, 50, 100], n2 = 4 — adds 2×/5× sub-ticks
  • Always emits decade boundaries (1, 10, 100, ...).
  • minor=true adds 2× and 5× sub-ticks per decade when there's room.
  • When the domain spans more decades than target allows, decades are sparse-skipped and minor is silently ignored.
  • Returns 0 if min <= 0 or max <= 0.

timeTicks(minMs, maxMs, target, out)

Generate ticks for a time axis. Returns a shared result object indicating which time unit was chosen — pass that unit straight to formatTime.

const out = new Float64Array(20);
const res = timeTicks(
    Date.UTC(2024, 0, 1),
    Date.UTC(2024, 6, 1),
    6,
    out
);
// res.unit  === TIME_UNIT.MONTH
// res.count === 7
// out[0..7] = monthly UTC timestamps

The function picks an interval from this ladder based on span and target:

| Span | Interval | |-----------------------|-----------------------------------------------------| | < 1 second | 1, 5, 10, 50, 100, 500 ms | | < 1 minute | 1, 5, 15, 30 s | | < 1 hour | 1, 5, 15, 30 min | | < 1 day | 1, 3, 6, 12 h | | < 1 month | 1 d, 2 d, 1 w, 2 w | | < ~6 months | calendar months (snapped to month boundaries) | | ≥ ~6 months | calendar years (snapped to year boundaries) |

⚠️ UTC contract: timeTicks and formatTime are strictly UTC. They never consult the host locale or DST rules. For exchange-local time (NYSE EST, Tokyo JST), apply the offset to your bounds before calling, then subtract it when mapping back to data coordinates.

⚠️ Shared result object: timeTicks returns a module-level singleton mutated in place. Read its fields (or copy them out) before calling again.

Label Thinning

thinLabels(ticks, count, pixels, minPixelSpacing, outIndices)

Given pre-mapped pixel positions, walk the tick array and keep only labels at least minPixelSpacing pixels apart. Greedy keep-from-left.

const ticks  = new Float64Array([10, 20, 30, 40]);
const pixels = new Float64Array([400, 380, 320, 310]); // Y-axis: descending
const idx    = new Int32Array(4);

const kept = thinLabels(ticks, 4, pixels, 50, idx);
// idx[0..kept] = [0, 2] — keeps tick @400px and @320px

Dimension-agnostic: works for X-axes (pixels increase) and Y-axes (pixels decrease) because the spacing check uses absolute distance.

Char-Code Formatters

All formatters write directly into a Uint8Array — perfect for SDF/MSDF text renderers like @zakkster/lite-bmfont that consume char codes.

formatNumber(value, decimals, out, offset)

const buf = new Uint8Array(16);
let n;

n = formatNumber(-12.56, 1, buf, 0);   // "-12.6"
n = formatNumber(99.999, 2, buf, 0);   // "100.00" (carry-over)
n = formatNumber(0, 2, buf, 0);        // "0.00"
n = formatNumber(NaN, 2, buf, 0);      // n === 0 (don't render)

formatCurrency(value, symbolCode, decimals, out, offset)

const buf = new Uint8Array(16);

let n = formatCurrency(1234.5, 36, 2, buf, 0);  // "$1234.50" — 36 = '$'
n = formatCurrency(7, 35, 0, buf, 0);            // "#7"        — 35 = '#'

If value is NaN/Infinity, returns 0. The symbol byte may have leaked into out[offset] — respect the return value and don't render. The stale byte is harmless (the buffer is caller-owned).

formatTime(ms, unit, out, offset)

| unit | Output | Bytes | |-------------------------|------------|-------| | MILLISECOND, SECOND | HH:MM:SS | 8 | | MINUTE, HOUR | HH:MM | 5 | | DAY, WEEK | MM/DD | 5 | | MONTH | MMM YY | 6 | | YEAR | YYYY | 4 |

const buf = new Uint8Array(16);
const ms = Date.UTC(2024, 2, 15, 14, 30, 45);

formatTime(ms, TIME_UNIT.SECOND, buf, 0);  // "14:30:45"
formatTime(ms, TIME_UNIT.MONTH,  buf, 0);  // "Mar 24"
formatTime(ms, TIME_UNIT.YEAR,   buf, 0);  // "2024"

TIME_UNIT

TIME_UNIT.MILLISECOND // 0
TIME_UNIT.SECOND      // 1
TIME_UNIT.MINUTE      // 2
TIME_UNIT.HOUR        // 3
TIME_UNIT.DAY         // 4
TIME_UNIT.WEEK        // 5
TIME_UNIT.MONTH       // 6
TIME_UNIT.YEAR        // 7

🔥 The full pipeline

import {
    timeTicks, formatTime, thinLabels
} from '@zakkster/lite-axis';

// All buffers allocated ONCE, reused every frame
const tickBuf  = new Float64Array(64);
const pixelBuf = new Float64Array(64);
const indexBuf = new Int32Array(64);
const charBuf  = new Uint8Array(16);

function renderAxis(minMs, maxMs, axisWidth) {
    // 1. Generate ticks
    const r = timeTicks(minMs, maxMs, 8, tickBuf);

    // 2. Map data → pixels
    const span = maxMs - minMs;
    for (let i = 0; i < r.count; i++) {
        pixelBuf[i] = ((tickBuf[i] - minMs) / span) * axisWidth;
    }

    // 3. Thin overlapping labels
    const kept = thinLabels(tickBuf, r.count, pixelBuf, 60, indexBuf);

    // 4. Format and draw kept labels
    for (let i = 0; i < kept; i++) {
        const idx = indexBuf[i];
        const n = formatTime(tickBuf[idx], r.unit, charBuf, 0);
        // drawFast(charBuf, n, pixelBuf[idx], y);   // your text renderer
    }
}

Zero allocations per frame. Predictable cache layout. No string objects ever created on the hot path.

🧪 Tests

npm test

68 tests covering:

  • Linear / log / time tick generation across all interval scales
  • Edge cases: min === max, reversed bounds, non-finite, negative ranges, sub-decimal log
  • Floating-point cleanup (no 0.30000000000000004)
  • Buffer-boundary correctness for every formatter
  • Label thinning across both pixel directions and contract violations
  • An end-to-end "tick → format" integration test

📦 TypeScript

Full declarations included in Axis.d.ts, including the TimeUnit type alias and TimeTicksResult interface.

📚 LLM-Friendly Documentation

See llms.txt for AI-optimized metadata and usage examples.

License

MIT