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

@protoyard/charts

v1.5.0

Published

Chart math for Protoyard prototypes: thin re-exports of @texuli/charts/math (scales, shapes, layouts, downsampling) plus theme-token chart palettes.

Readme

@protoyard/charts

Framework-agnostic chart math for Protoyard prototypes: scales, shapes, downsampling, layouts and theme-token palettes. No Svelte and no third-party chart library (ADR-021), so everything here is plain TypeScript tested in Node. Svelte components live in @protoyard/charts-ui; the platform guide is docs/CHARTS.md.

Entry points

sideEffects: false; import a subpath to pull in only that module.

| Import | Contents today | | --- | --- | | @protoyard/charts | Everything below plus the data contract types (ChartPoint, ChartSeries, Extent, Margin, Rect) | | @protoyard/charts/scales | linearScale, logScale, sqrtScale, timeScale (UTC), bandScale, pointScale; ticks, tickStep, niceDomain, timeTicks, chooseTimeInterval, floorTime; formatters formatNumber, formatCompact, formatTick, formatCents (integer cents only), formatPercent, formatTimeTick, formatDate; extent | | @protoyard/charts/shapes | linePath(points, { curve, defined }) and areaPath(points) with linear, monotone-x, step, step-before, step-after curves and gaps; stack(series, { offset: 'none' \| 'expand' \| 'diverging' }), stackExtent, groupOffsets(bandwidth, n, padding); pie(values, { padAngle, sort }), arcPath({ innerRadius, outerRadius, startAngle, endAngle, cornerRadius, padAngle }), arcCentroid; symbolPath(type, x, y, area) (circle, square, diamond, triangle, cross); bin(values, { thresholds \| count, domain }) with Sturges default; roundCoord | | @protoyard/charts/downsample | largestTriangleThreeBuckets(points, threshold) (keeps gaps, returns the input when small), lttbIndices(xs, ys, threshold) (plain or typed arrays), minMaxBuckets(xs, ys, { buckets, x0?, x1? }) (first/min/max/last per pixel column, for canvas), bisectX / bisectLeft (O(log n) crosshair lookup), downsampleThreshold(plotWidthPx) (2 per pixel, 200–4,000) | | @protoyard/charts/layout | treemap(root, { width, height, padding, paddingTop }) (squarified; validates sums, TreemapError); sankey(nodes, links, { width, height, nodeWidth, nodePadding, iterations }) (DAG only, SankeyError with code: 'cycle' \| 'unknown-node' \| 'invalid-value' \| 'duplicate-node'); funnelLayout(steps, { width, height, orientation, gap }) (geometry + conversion/drop-off); calendarLayout({ start, end }, { values, weekStartsOn }) (UTC week columns × weekday rows, missing days null); matrixLayout(rows, cols, { width, height, gap }); gaugeLayout(value, { min, max, startAngle, endAngle, thresholds }); plotArea | | @protoyard/charts/palette | chartPalette(themePalette, mode) → 8 categorical + 9-step sequential + 9-step diverging hex colors, repaired to pass; validateChartPalette(); chartPaletteVariables() (--tx-chart-1…8, --tx-chart-sequential-1…9, --tx-chart-diverging-1…9 for a chart root); readChartPaletteInput(); seriesColor(i), sequentialColor(t), divergingColor(t); color math (contrastRatio, OKLCH, simulateDeficiency) | | @protoyard/charts/palette/derive | The palette builder alone (chartPalette, validateChartPalette), for lazy import(): chart frames load it only for themes without a chart block. Sizes and thresholds live in palette/constants.ts, so static imports of @protoyard/charts/palette never pull it in. |

Scales are complete (PLAN_003 T-charts-scales): the whole ./scales module is 3.1 KB gzip (esbuild, minified, gzip -9); a typical import of linearScale + ticks + formatCompact is 0.9 KB. Downsampling is complete (T-charts-downsample, 1.2 KB gzip for the whole ./downsample module). Measured in Node 26.11.1 on an Apple M1 Max (median of 9, three runs of a PROTOYARD_BENCH=1 downsample test; that test was removed in PLAN_009 because the math is tested upstream in @texuli/charts/math): LTTB 10k → 1k points 0.29–0.71 ms, 100k → 2k 1.27–1.37 ms; min-max 100k points into 1,600 columns 2.6–4.0 ms; 10,000 bisectX lookups on 100k points 1.3–1.6 ms. Shapes are complete (T-charts-shapes): the whole ./shapes module is 2.9 KB gzip; linePath + areaPath + stack alone are 1.1 KB, pie + arcPath 0.9 KB. Shapes work in pixel space (scale the data first), use radians clockwise from 12 o'clock for arcs, round every coordinate to two decimals, and treat null/NaN y values as gaps (lines break, never drop to 0). Layouts are complete (T-charts-layouts), each tree-shaken on its own: treemap 1.0 KB, sankey 1.5 KB, funnel 0.4 KB, calendar + matrix 0.7 KB, gauge 0.4 KB gzip (3.5 KB for all). The sankey puts nodes in columns by longest path from the sources, sinks in the last column, then runs six relaxation passes. Node heights equal their flow on both sides. Gauge angles use the arcPath convention, so its bands and value arc draw directly with arcPath.

Chart palettes

chartPalette() starts from the theme's chart1…chart5 and adds three hues placed as far as possible from them. Every color that marks data is checked and, when needed, repaired by moving lightness only, so hues stay recognizable:

| Check | Rule | | --- | --- | | Contrast | Every categorical color and each ramp's data end ≥ 3:1 against surface and background (WCAG 1.4.11) | | Distinct | Any two categorical colors ≥ 0.08 apart in OKLab | | Colour vision | Any two categorical colors ≥ 0.04 apart under simulated protanopia and deuteranopia (Machado et al. 2009) | | Ramps | Sequential lightness moves monotonically toward the data end; diverging has a neutral midpoint |

Gray tokens (a monochrome theme) can only differ in lightness. When they end up too close, they are spread evenly over the lightness band that keeps 3:1. palette.repaired names the theme tokens that were changed. Every built-in and library theme passed in both modes after repair when the every-theme palette sweep ran; PLAN_009 removed that sweep, and packages/charts/tests/charts.test.ts keeps the CSS-variable palette case. Color alone never identifies a series: legends and labels stay required.

Size and speed: building a palette costs 2.9 KB gzip (chartPalette + chartPaletteVariables + seriesColor; the whole module with validation is 3.6 KB). seriesColor/sequentialColor alone are 0.3 KB. One palette takes about 0.3 ms median and up to 1.5 ms after warm-up in Node (Apple M1 Max). Compute it once per appearance change, not per render.

Rules

  • Color comes only from theme variables (--tx-chart*), so light/dark and every theme work without recomputation.
  • No Date.now() or Math.random(): time and data come from the caller.
  • Time scales, ticks and date formatters work in UTC, so labels do not move with the viewer's time zone or daylight saving. Formatters default to en-US; pass locale to change.
  • formatCents throws on a non-integer: money is stored as integer cents.
import { formatCents, formatTimeTick, linearScale, timeScale } from '@protoyard/charts/scales';

const x = timeScale({ domain: [start, end], range: [0, width], nice: true });
const y = linearScale({ domain: [0, maxCents], range: [height, 0], nice: true });
const interval = x.tickInterval(6);
const xLabels = x.ticks(6).map((t) => formatTimeTick(t, interval));
const yLabels = y.ticks(5).map((v) => formatCents(Math.round(v), 'USD', { compact: true }));

Tests

pnpm vitest run packages/charts
pnpm --filter @protoyard/charts typecheck

Package output

The manifest keeps TypeScript source exports for workspace development. pnpm build:packages --filter engine creates the ESM and declaration files under dist/ that the packed manifest exposes.