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

fcharts-js

v0.3.0

Published

Charts that render 100k+ points at 60fps and pass the accessibility audit at the same time.

Readme

fcharts

Charts that render 100k+ points at 60fps and pass the accessibility audit — at the same time.

Every fast JS chart library throws away accessibility (canvas/WebGL with no keyboard nav, no screen-reader output, no find-in-page). Every accessible one (SVG/DOM-per-point) falls over at scale. fcharts is a validation MVP built to prove you can have both: a min/max downsample renderer on <canvas> (frame cost ≈ O(viewport width), independent of point count) plus a real-DOM accessibility layer overlaid on top.

A 100,000-point fcharts chart driven entirely by keyboard: the focus ring, the data cursor stepping through samples with its value readout, keyboard zoom, and — mirrored below the plot — the live-region announcement a screen reader speaks for every move.

  • Zero runtime dependencies. ~21 KB min+gzip for the canvas core (tree-shaken); ~39 KB with every SVG primitive, locale pack, and integration helper included.
  • Fast. A precomputed min/max pyramid keeps per-frame cost flat from 10k to 250k+ points.
  • Accessible by default — not a flag. Keyboard-navigable data cursor with aria-live announcements, real-text axis ticks, an accessible legend, and a hidden data <table> alternative. All find-in-page-able and translatable.
  • Works today, no browser flags. HTML-in-Canvas is feature-detected and used if present; otherwise the DOM-overlay path runs in stock Chrome/Firefox/Safari.

This is a validation MVP, not a product. See FINDINGS.md for the benchmark numbers and an honest verdict on the thesis.

Install

npm install fcharts-js       # ESM, for bundlers (Vite, webpack, esbuild…)
<!-- Zero build: drop in the UMD bundle (global namespace `fcharts`) -->
<script src="https://unpkg.com/fcharts-js/dist/fcharts.umd.cjs"></script>
<script>const { FChart } = window.fcharts;</script>

Evaluating without publishing? Build a local, installable tarball: pnpm pack:sdkfcharts-js-0.3.0.tgz, then npm install ./fcharts-js-0.3.0.tgz. Or open examples/quickstart.html directly (no server, no build).

Quickstart

import { FChart } from 'fcharts-js';

const el = document.getElementById('chart')!; // a sized container

const chart = new FChart(el, {
  series: [
    { name: 'Pressure', color: '#16a34a' },
    { name: 'Temperature', color: '#d97706', type: 'area' },
    { name: 'Vibration', color: '#0ea5e9' },
  ],
  options: { ariaLabel: 'Sensor telemetry', xLabel: 'sample', yLabel: 'value' },
});

// x is shared and non-decreasing; one y array per series, all the same length as x.
const N = 100_000;
const x = Float64Array.from({ length: N }, (_, i) => i);
const pressure = Float64Array.from(x, (i) => 40 + Math.sin(i * 9e-4) * 26);
const temperature = Float64Array.from(x, (i) => 5 + Math.sin(i * 2.1e-3) * 18);
const vibration = Float64Array.from(x, (i) => -32 + Math.sin(i * 4e-3) * 22);
chart.setData({ x, y: [pressure, temperature, vibration] });

Then prove it's accessible — point the auditor at the page you just built (it runs the functional checks an axe scan skips: keyboard, live region, contrast, target size):

npx -p fcharts-js -p playwright fcharts-audit --target http://localhost:5173 --selector '#chart'

Sizing

The container just needs a size (e.g. #chart { width: 100%; height: 420px }). Styles are injected automatically — no CSS import required.

The chart fills its container (.fc-root is height: 100%), so height: 100% needs a definite-height ancestor — inside an auto-height parent it resolves to 0 and nothing renders (the chart warns once in the console when it measures a zero height). Two pitfalls: a height:100% mount with no fixed-height ancestor, and constructing the chart while the container is display:none. The safe pattern for an indefinite container is a definite-height box the mount fills absolutely:

<figure style="position:relative; height:250px">
  <div id="chart" style="position:absolute; inset:0"></div>
</figure>

Real-time / streaming

For live data, append adds one sample without rebuilding anything — amortized O(1) (only the tail of the min/max pyramid updates), so cost stays flat as the series grows to 100k+ points.

// x must be >= the current last x (non-decreasing); one y per series.
setInterval(() => {
  lastX += 1;
  lastPrice += (Math.random() - 0.5) * 0.5;
  chart.append(lastX, [lastPrice]);
}, 100);

The y-domain auto-fits new highs/lows, and the view follows the live tail when it's already showing it — a zoomed window slides (keeping its width), a full-history view expands. If the user has panned back into history, the view stays put so they can keep reading the past.

Accessibility: the library never auto-updates on its own, so driving append on a timer makes auto-updating content — give users a Pause/Stop control and respect prefers-reduced-motion (WCAG 2.2.2). The landing-page hero demonstrates this.

For a worked example against a real exchange feed — Hyperliquid trades aggregated into live OHLC candles (append per bucket + amendLast for the forming one) with a volume panel, interval switching, and range presets — open examples/hyperliquid-live.html (no server, no build).

Interaction

| Input | Action | | --- | --- | | Scroll / wheel | Zoom the x-axis around the pointer | | Drag | Pan | | Tab to chart, then | Move the cursor between samples | | | Switch series | | Home / End | Jump to the first / last sample | | Shift + arrow | Fine (single-sample) step | | + / - | Zoom in / out (keyboard equivalent of the wheel) | | Esc | Dismiss the cursor / readout (keeps focus on the chart) | | Legend buttons | Toggle series visibility |

Every cursor move is announced through a polite live region (coalesced so holding a key doesn't flood it). Axis ticks are real visible text — always Ctrl+F-findable — and the hidden data table is in the accessibility tree (and find-in-page-able in Chromium).

API

new FChart(el: HTMLElement, config: FChartConfig)

chart.setData({ x, y })          // replace data, reset the view
chart.append(x, [y0, y1, …])      // append one sample, O(1) — real-time/streaming
chart.amendLast([y0, y1, …])      // rewrite the last sample in place, O(log n) — forming candles
chart.update({ series, options }) // patch series/options in place
chart.renderSync(domain?)         // synchronous render (programmatic zoom / capture)
chart.summary()                   // structured ChartSummary (see "Agent-readable")
chart.toCSV()                     // full dataset as CSV (candles → four OHLC columns)
chart.onDomainChange(cb)          // x-domain subscription (zoom/pan); returns unsubscribe
chart.destroy()                   // remove DOM, listeners, observers
chart.renderPath                  // 'dom-overlay' | 'html-in-canvas'
chart.htmlInCanvas                // { supported, via }

Agent-readable

The accessibility layer is the same substrate AI agents and crawlers read. Beyond the live region and data table, each chart distills its data into a structured summary — exposed three ways:

  • chart.summary()ChartSummary (per-series min/max/first/last/mean, change, trend).
  • a one-line natural-language description on the focusable surface (aria-describedby), so screen readers and agents get the values and trend on focus.
  • an embedded <script type="application/json" data-fcharts> block any DOM scraper can parse — where a <canvas> chart is an opaque bitmap.

Config

interface SeriesConfig {
  name: string;
  color: string;
  type?: 'line' | 'area' | 'candle'; // default 'line'
  visible?: boolean;        // default true
  width?: number;           // line width px, default 1.25
  fillAlpha?: number;       // area fill, default 0.15
  upColor?: string;         // candle body, close >= open (default palette green)
  downColor?: string;       // candle body, close < open (default palette red)
}

A candle series consumes four y arrays — open, high, low, close — at its position in data.y (line/area take one each). Up candles draw hollow, down candles filled, so direction never relies on colour alone; past ~1 candle per 3px the series degrades to the exact per-column high/low envelope. The cursor, live region, hidden table, and summary() all speak OHLC. Stream live candles with append (new bucket) + amendLast (forming bucket) — see the Hyperliquid example.


interface FChartOptions {
  ariaLabel?: string;
  xLabel?: string; yLabel?: string;
  legend?: boolean;         // default true
  maxDpr?: number;          // device-pixel-ratio cap, default 2
  yPadding?: number;        // y-extent padding fraction, default 0.06
  xInteger?: boolean;       // integer index ticks, default false
  xType?: 'linear' | 'time';  // 'time': x = epoch ms → calendar ticks + date labels
  yScale?: 'linear' | 'log';  // 'log': base 10, needs positive data
  exportControl?: boolean;  // visible "Download data (CSV)" button, default false
  xTickCount?: number; yTickCount?: number;
  locale?: string;          // BCP-47 tag: Intl-localized DEFAULT tick/value formatters
                            // (explicit formatX/formatY always win; unset = English defaults)
  formatX?: (v: number) => string;
  formatY?: (v: number) => string;
  reducedMotion?: boolean;  // auto-detected from prefers-reduced-motion
  highContrast?: boolean;   // auto-detected from prefers-contrast
  strings?: Partial<FChartStrings>;  // localize the fixed UI text (legend, keyboard help,
                                        // data summary, table caption) for non-English pages
}

Theming

A handful of CSS custom properties on the container (or :root) cover theming:

.fc-root {
  --fc-ink: #e7edf3;            /* DOM text */
  --fc-tick-color: #7d8b99;     /* axis tick labels */
  --fc-grid: rgba(255,255,255,.07);   /* canvas grid lines */
  --fc-axis: rgba(255,255,255,.14);   /* canvas axis/border */
  --fc-cursor: rgba(255,255,255,.4);  /* canvas crosshair */
  --fc-focus: #6ee7a8;          /* keyboard focus ring */
}

Adapters & integrations

Everything below is a thin layer over the same imperative class — one engine, one a11y layer.

  • React / Vue / Sveltefcharts-js/react and fcharts-js/vue export a declarative <FChart> component; fcharts-js/svelte exports a dependency-free action (<div use:fchart={{ series, data }} />). Same contract everywhere: identity-changed props forward via update(), a change to a construction-fixed option remounts cleanly. React/Vue are optional peer deps on separate entries — the core never pulls a framework in. One-click starters for all four integrations: examples/ (each opens directly in StackBlitz).
  • Web componentdefineFChart() registers <f-chart> (light DOM, so the a11y layer stays in the page's accessibility tree); configure via the element's config property.
  • SSR + hydration — render on the server with renderSVG(config, data, { width, height }) (pure, Node-safe; donut/scatter/sparkline/bars/progress/heatmap builders included), then upgrade in place with hydrate(container, config) — synchronous, no layout shift; the static SVG is real, agent-readable content before any JS runs.
  • Linked panessyncCharts([price, volume]) shares the x-window across charts (zoom/pan any pane, the others follow); built on the public chart.onDomainChange().
  • Time & log axesxType: 'time' puts ticks on calendar boundaries with adaptive date labels; yScale: 'log' gives decade ticks (positive data).
  • Localization — complete stringsDE/stringsFR/stringsES packs for options.strings (the a11y layer's fixed prose; WCAG 3.1.2), and options.locale (any BCP-47 tag) to localize the default tick/value formatters via Intl — dates, month names, decimal separators. The two compose: { locale: 'de', strings: stringsDE } localizes a chart end to end.
  • fcharts-render CLIfcharts-render spec.json > chart.svg (or stdin): charts from shells, report pipelines, and agents with no browser and no code.

Accessibility & the Compliance Pack

The MIT renderer is accessible by construction. The Compliance Pack is the paid layer on top — the proof, kept current automatically. (Both live in this repo; the Pack is a separate entry, src/compliance/, never bundled into the core chart.)

  • A per-criterion WCAG 2.2 AA evidence map for the chart layer, adversarially verified, with file:line evidence: 33 Supports / 2 Partially Supports / 20 Not Applicable across the 55 Level A + AA success criteria. The honest 2 partials are inherently integrator-dependent (1.4.3 text on the host background, 2.4.11 focus not obscured by host UI). See compliance/scope-and-evidence-map.md.
  • An auto-generated VPAT/ACR (Accessibility Conformance Report) — EN 301 549 (EU/EAA), WCAG, and Section 508 editions, as Markdown + HTML + JSON, from one dependency-free generator. Sample: compliance/samples/. Editions, the functional-performance derivation, and the DRAFT/attestation model are in compliance/vpat-editions.md.
  • A CI accessibility gate (fcharts-audit) that runs the conformance engine against your real configured chart on every commit and fails the build on any regression below the baseline — the thing a whole-site scanner does badly: prove a complex interactive component stays per-point accessible. See compliance/conformance-test-plan.md and compliance/ci-gate.md.
  • Audit a chart you don't own: npx fcharts-audit --target https://your.app/dashboard --selector '#chart' points the same functional checks (keyboard, live region, contrast, target size) at any existing chart — Highcharts, ECharts, a bare canvas — and reports exactly which ones fail. Report-only, no baseline needed.
  • Diff two ACRs: fcharts-audit --compare old/acr-en301549.json new/acr-en301549.json answers procurement's actual question — what changed in conformance between versions — criterion by criterion (improved / regressed / scope-changed / remarks-only), exiting non-zero when a claim weakened. Pure JSON diff: needs neither Playwright nor Vite.
  • Real screen-reader testing in CI: pnpm test:at drives actual screen readers over a live chart (via guidepup) and asserts on the phrases they speak. The NVDA suite runs green on every push to main (Windows runner): the chart's accessible name + data summary, per-sample arrow-key value announcements, and legend toggle state, as NVDA actually renders them. A VoiceOver sibling suite runs as monitoring on macOS (Apple's CI images don't yet capture VoiceOver speech). Both in .github/workflows/at.yml; both skip cleanly on machines without screen-reader automation.

Why a gate and not just a scan? The benchmark's own finding (see FINDINGS.md): a bare inaccessible <canvas> scores 0 axe violations too. Real conformance needs functional checks — keyboard, live region, focus, computed contrast, target size — which is what the engine runs.

GitHub Action

The audit ships as a reusable action (action.yml) — gate any repo's charts in a few lines, no fcharts adoption required:

# Audit any chart on a live page (report-only; Highcharts, ECharts, bare canvas, …)
- uses: Fbartoli/[email protected]
  with:
    target: https://preview.example.com/dashboard
    selector: '#price-chart'

# Or gate an fcharts chart against its committed baseline (fails the build on regression)
- uses: Fbartoli/[email protected]
  with:
    fixture: ./a11y/fixture.ts
    editions: en301549 wcag

Both modes upload the generated report as a build artifact. Everything installs into an isolated prefix — your package.json and node_modules are untouched.

Worked CI example

Point the gate at a fixture that builds your real chart (export mountChart(el) => teardown):

# .github/workflows/a11y-gate.yml
name: a11y gate
on: [pull_request, push]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '24' }
      - run: npm ci
      - run: npx playwright install --with-deps chromium   # dev/peer dep, CI-only
      - run: npx fcharts-audit --fixture ./a11y/fixture.ts --edition en301549 --out ./compliance-out
      - if: always()
        uses: actions/upload-artifact@v4
        with: { name: accessibility-conformance-report, path: ./compliance-out }

This repo dogfoods its own gate in .github/workflows/a11y-gate.yml (invoking node src/compliance/cli.ts directly). Run it locally with pnpm a11y-audit.

Architecture

src/
  core/        scales · ticks · downsample (min/max pyramid) · scheduler · model
  a11y/        cursor · live-region · legend · table-alt · ticks · summary · strings · styles
  renderers/   canvas2d · html-in-canvas · detect · renderer (interface)
  compliance/  WCAG baseline · conformance engine · contrast · ACR generator · fcharts-audit CLI
  fchart.ts public class — wires core + renderer + a11y

core/ and compliance/ import no rendering API; compliance/ (the paid Pack) imports Playwright as a dev/peer dependency only — the shipped renderer stays zero-runtime-dependency.

The core/ modules never import a rendering API, so a WebGL backend can implement the same Renderer interface later without touching the public API.

Develop

pnpm install          # pnpm 11+ (cooldown + script-blocking for supply-chain safety)
pnpm test             # unit tests on Node's built-in test runner (no test framework dep)
pnpm test:browser     # interaction tests (keyboard/wheel/drag/streaming) in headless Chromium
pnpm test:at          # real VoiceOver assertions via guidepup (macOS; skips elsewhere)
pnpm typecheck
pnpm lint             # oxlint: correctness + suspicious + perf, zero findings allowed
pnpm check            # typecheck + lint + unit + browser tests (what CI runs)
pnpm build            # dist/fcharts.js (ESM) + .d.ts
pnpm dev              # serve the benchmark page
pnpm bench            # headless FPS + axe-core run (Chromium) → bench/results.json
node bench/harness.ts firefox   # or `webkit` — cross-browser run → results-<engine>.json
pnpm a11y-audit       # run the WCAG 2.2 AA conformance gate → ACR in ./compliance-out
pnpm size             # assert the bundle stays under 45 KB gzip