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

declarative-charts

v0.3.0

Published

Charts written as HTML elements, one element per datapoint. Bar, line, area, bubble, scatter, pie, funnel, stage and radar, with axes, legends, patterns and accessibility.

Readme

Declarative Chart Library

CI npm License: MIT

Write (and read!) charts in plain HTML.

Most chart libraries want an empty <div> and a call into an opaque JavaScript library. The data comes either from an extra endpoint or hard-coded into a config object, so a simple chart ends up spread across HTML, JavaScript, and often a server route as well.

This library lets you write charts declaratively in HTML:

<dc-chart width="600" height="400">
  <dc-title>Revenue by Region</dc-title>
  <dc-bar value="4200" label="North"></dc-bar>
  <dc-bar value="3800" label="South"></dc-bar>
  <dc-bar value="5100" label="East"></dc-bar>
</dc-chart>

That image is the output of the markup above, rendered by the library and exported with its own downloadSvg().

Which means your existing template loop already knows how to write it:

<!-- Jinja2 / Flask -->
<dc-chart width="600" height="400">
  <dc-title>Revenue by Region</dc-title>
  {% for region in regions %}
    <dc-bar value="{{ region.revenue }}" label="{{ region.name }}"></dc-bar>
  {% endfor %}
</dc-chart>
<!-- Django - the same loop, with a Django filter doing the rounding -->
<dc-chart width="600" height="400">
  <dc-title>Revenue by Region</dc-title>
  {% for region in regions %}
    <dc-bar value="{{ region.revenue|floatformat:0 }}" label="{{ region.name }}"></dc-bar>
  {% endfor %}
</dc-chart>
<%# Rails %>
<dc-chart width="600" height="400">
  <dc-title>Revenue by Region</dc-title>
  <% @regions.each do |region| %>
    <dc-bar value="<%= region.revenue %>" label="<%= region.name %>"></dc-bar>
  <% end %>
</dc-chart>

No endpoint to build, no serialisation to keep in step with your models, and the chart is in your page's source where you can read (and understand) it.

It updates the way the rest of your page does

A chart watches its own children, so changing the markup is the API. There is no redraw(), no setData(), and no requestUpdate() to remember.

Swap the whole thing — here a server-rendered fragment, fetched and dropped in:

<dc-chart id="sales" width="600" height="400"></dc-chart>
const chart = document.querySelector('#sales');
chart.innerHTML = await (await fetch('/reports/q3')).text();
// Nothing further. The chart notices and redraws.

Or change one part of it. Each of these is the entire update:

// Add a bar
chart.insertAdjacentHTML('beforeend', '<dc-bar value="5100" label="East"></dc-bar>');

// Revise a value
chart.querySelector('dc-bar[label="North"]').setAttribute('value', '4800');

// Hide a series - the legend drops it too
chart.querySelector('dc-line[label="2023"]').hidden = true;

Because it is a MutationObserver over ordinary DOM rather than a framework binding, nothing above is specific to how you got there. The same three lines work from an event handler, from htmx or Turbo swapping the children out, from Alpine, or from a <template> you cloned — the chart never learns which.

Every element passes attributes it does not recognise — data-*, htmx's hx-*, Alpine's x-on:, anything — through to the SVG shape, so a bar can be a drop target or trigger a request. (Inline on* handlers are the one exception; see API.md.)

Where this sits

The useful distinction between charting libraries is not "web component or JS library" — that is cosmetic. It is where the data lives, and on that axis there are only two designs.

Data as a payload. One element, or one call, per chart:

<google-chart data='[["Month","Days"],["Jan",31],["Feb",28]]'></google-chart>
new Chart(ctx, { data: { labels: [...], datasets: [...] } });

Different clothes, same design. The data is an opaque blob you must serialise first, and the HTML is a mounting point. You cannot address one datapoint from markup, attach a handler to a single bar, or let a template emit a row without JSON-encoding it. Chart.js, ECharts, ApexCharts, Highcharts, Recharts, Vega-Lite and the web-component wrappers all work this way.

Data as markup. One element per datapoint:

<td style="--size: 0.4"><span class="data">$40K</span></td>   <!-- Charts.css -->
<dc-bar value="40000" label="Jan"></dc-bar>                    <!-- this library -->

That category has essentially two members. Charts.css proved there is demand for it — 6.5k stars — and stopped where real charting begins, because a stylesheet cannot measure text, compute a nice axis range, lay out a legend, fit labels, or handle interaction. Its markup also asks you to pre-normalise: --size is a ratio from 0 to 1, so the real number never appears in the document except as display text.

This library is the data-as-markup approach with a rendering engine behind it — axes, scales, legends, text fitting, interaction, accessibility — and value="40000" is the actual number, un-normalised, so your template does not need to know the series maximum before it can emit a row.

The engine is the trade. Charts.css needs no JavaScript and this does, so with scripting off a <dc-chart> shows nothing at all. If that matters for your page, supply a fallback table — either in <noscript>, or inside the chart where a dc-chart:defined rule hides it once the library arrives, which also covers a bundle that was blocked or failed to load. Both are plain HTML and neither needs an API: When JavaScript Does Not Run.

More than bars

<dc-chart width="600" height="400">
  <dc-axis position="bottom"><dc-title>Dose (mg)</dc-title></dc-axis>
  <dc-reference min="40" max="60" fill="#16a34a" label="Target range"></dc-reference>
  <dc-scatter label="Control" fill="#2563eb">
    <dc-point x="5" value="12"></dc-point>
    …
  </dc-scatter>
  <dc-scatter label="Treated" fill="#dc2626" shape="triangle">…</dc-scatter>
  <dc-legend position="bottom"></dc-legend>
</dc-chart>

Bar, line, area, bubble, scatter, pie, funnel, stage and radar, with axes, legends, patterns, annotations and keyboard navigation. The examples have all of it.

Where it fits, and where it does not

The sweet spot is small-to-medium categorical data emitted by a server template — a few dozen bars, a handful of series. One element per datapoint has real costs at scale: DOM weight, verbosity, and markup that gets unwieldy past a few hundred points.

Rendering scales close to linearly. On one laptop, a line chart paints 1,000 points in about 170 ms, 5,000 in about 580 ms, and 10,000 in about 1.2 s. A few thousand points is comfortable; very large series are where you start to notice the page working.

Measure it on your own device — that page is the honest answer, since a phone is slower than a laptop and the difference matters more than any figure published here.

A fuller version of this analysis, with the competitive matrix and its caveats, is in docs/review.md.

Features

  • Declarative Syntax - Define charts with nested HTML elements
  • Multiple Chart Types - Bar, Line, Area, Bubble, Pie, Funnel, and Stage charts
  • Rich Styling - Colors, gradients, patterns, and high contrast mode
  • Number Formatting - Currency, compact (1.2M), percentages, locale-aware
  • Fully Accessible - ARIA labels, keyboard navigation, screen reader support
  • Interactive - Popups, clickable elements, dynamic updates
  • Events - dc-click, dc-mouseenter, dc-mouseleave, dc-render with typed detail
  • Lightweight - Built on Lit (~5KB overhead)
  • TypeScript - Full type definitions included
  • Framework-agnostic - Works with React, Vue, Angular, or plain HTML

Quick Start

Installation

Via npm:

npm install declarative-charts lit
import 'declarative-charts';

Lit is a peer dependency, so you install it alongside. This keeps a single copy of Lit in your app if you already use it — bundling our own would give you two. The CDN builds below need no such thing; they are fully self-contained.

Via CDN:

<script type="module" src="https://unpkg.com/declarative-charts"></script>

Or with jsdelivr:

<script type="module" src="https://cdn.jsdelivr.net/npm/declarative-charts"></script>

Local development:

git clone https://github.com/LarryLustig/declarative-charts.git
cd declarative-charts
npm install
npm run dev

Open your browser to http://localhost:5173

Bar Chart

<dc-chart width="600" height="400">
  <dc-title>Monthly Sales</dc-title>
  <dc-bar value="10" fill="red" label="Jan"></dc-bar>
  <dc-bar value="25" fill="blue" label="Feb"></dc-bar>
  <dc-bar value="15" fill="green" label="Mar"></dc-bar>
</dc-chart>

Line Chart

<dc-chart width="600" height="400">
  <dc-title>Temperature Trends</dc-title>
  <dc-line stroke="#9C27B0" label="City A">
    <dc-point value="15" label="Mon"></dc-point>
    <dc-point value="18" label="Tue"></dc-point>
    <dc-point value="22" label="Wed"></dc-point>
  </dc-line>
</dc-chart>

Area Chart

<dc-chart width="600" height="400">
  <dc-title>Traffic by Source</dc-title>
  <dc-area fill="#2196F3" label="Direct">
    <dc-point value="30" label="Mon"></dc-point>
    <dc-point value="45" label="Tue"></dc-point>
    <dc-point value="38" label="Wed"></dc-point>
  </dc-area>
  <dc-area fill="#4CAF50" label="Referral">
    <dc-point value="20" label="Mon"></dc-point>
    <dc-point value="25" label="Tue"></dc-point>
    <dc-point value="32" label="Wed"></dc-point>
  </dc-area>
</dc-chart>

Areas stack by default; add overlapping to the chart to draw them on top of one another.

Pie Chart

<dc-pie-chart width="600" height="400">
  <dc-title>Market Share</dc-title>
  <dc-pie-slice value="45" label="Product A"></dc-pie-slice>
  <dc-pie-slice value="30" label="Product B"></dc-pie-slice>
  <dc-pie-slice value="25" label="Product C"></dc-pie-slice>
</dc-pie-chart>

Funnel Chart

<dc-funnel-chart width="600" height="400">
  <dc-title>Conversion Funnel</dc-title>
  <dc-funnel-stage value="1000" label="Visitors"></dc-funnel-stage>
  <dc-funnel-stage value="500" label="Leads"></dc-funnel-stage>
  <dc-funnel-stage value="100" label="Customers"></dc-funnel-stage>
</dc-funnel-chart>

Stage Chart

<dc-stage-chart width="400" height="500" stage-size="value">
  <dc-title>Pipeline</dc-title>
  <dc-stage value="100" label="Leads"></dc-stage>
  <dc-stage value="60" label="Qualified"></dc-stage>
  <dc-stage value="20" label="Won"></dc-stage>
</dc-stage-chart>

Like a funnel, a stage chart shows a flow — but it draws each step as a shape whose area is proportional to its value, rather than as a chevron band.

Bubble Chart

<dc-chart width="600" height="400">
  <dc-title>Quarterly Performance</dc-title>
  <dc-bubble label="Q1" value="30" size-value="100"></dc-bubble>
  <dc-bubble label="Q2" value="45" size-value="200"></dc-bubble>
  <dc-bubble label="Q3" value="60" size-value="300"></dc-bubble>
</dc-chart>

Browser Compatibility

| Browser | Supported Versions | |---------|-------------------| | Chrome | 90+ | | Firefox | 90+ | | Safari | 14+ | | Edge | 90+ |

Requires native support for Web Components (Custom Elements, Shadow DOM) and ES2020. No polyfills needed for modern browsers.

Bundle Size

Three artifacts, because three kinds of consumer need different things. You download exactly one.

| Artifact | Lit | Raw | Gzipped | For | |---|---|---|---|---| | declarative-charts.standalone.js | inlined | 291 KB | 75 KB | CDN and vendored <script type="module"> | | declarative-charts.umd.cjs | inlined | 295 KB | 75 KB | plain <script src> and require() | | declarative-charts.js | external | 456 KB | 108 KB | bundlers — yours will minify it |

Lit accounts for about 6 KB gzipped of the self-contained builds; the library itself is ~69 KB gzipped. The bundler-facing build is larger because it keeps /* @__PURE__ */ annotations and formatting for your bundler to tree-shake against and minify — it is not what a browser downloads.

Figures from npm run build, which prints them on every build.

Performance Guidelines

  • Large datasets: For very large datasets, consider aggregating data before rendering.
  • Dynamic updates: Just change the markup. Charts watch their own children with a MutationObserver, so adding, removing, hiding or re-attributing an element re-renders automatically — requestUpdate() is not needed, and htmx-style innerHTML swaps work as-is.
  • Hidden elements: Use the hidden attribute on data elements to temporarily hide them without removing from DOM.
  • Popups: Use auto-popup for automatic tooltips instead of manually managing popup state.
  • SVG rendering: Charts render to SVG, which scales cleanly but can slow down with thousands of elements.

Documentation

Those pages are built from examples/ and published on every push to main. To run them locally, npm run dev and open localhost:5173.

Key Examples

| Feature | Example | |---------|---------| | Bar charts | barcharts.html | | Line charts | linecharts.html | | Pie charts | piecharts.html | | Funnel charts | funnelcharts.html | | Colors & gradients | colors.html | | Patterns & palettes | palettes.html, patterns.html | | Number formatting | formatting.html | | Legends & titles | legends.html, titles.html | | Axes configuration | axes.html | | Popups & interactivity | popups.html, interactive.html | | Accessibility | accessibility.html | | htmx integration | htmx-integration.html |

Development

npm run dev      # Start development server
npm run build    # Build for production
npm test         # Run tests

Project Structure

src/
├── chart.ts            # Bar/Line/Bubble chart component
├── pie-chart.ts        # Pie chart component
├── funnel-chart.ts     # Funnel chart component
├── chart-*.ts          # Data elements (bar, line, point, etc.)
└── index.ts            # Main export

examples/               # Example HTML files

License

MIT - See LICENSE for details.