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

@exodus/datapoints-react

v2.1.0

Published

Headless React components for provenance-backed datapoint reports: unstyled, data-attribute driven, built on the @exodus/datapoints contract

Downloads

436

Readme

@exodus/datapoints-react

Headless React components for provenance-backed datapoint reports. Renders the @exodus/datapoints report payload — prose with value pills and a verify table with per-datapoint audit panels — with zero styles. Every visual state is exposed as a data-* attribute, so any design system styles it with plain CSS.

Install

npm install @exodus/datapoints-react

React 19 is a peer dependency. @exodus/datapoints (the payload contract) comes along as a regular dependency.

Rendering a report

import { DataReport } from '@exodus/datapoints-react';

function Report({ payload, sourceText }: { payload: unknown; sourceText?: string }) {
  return (
    <DataReport.Root payload={payload} text={sourceText}>
      <DataReport.Prose />
      <DataReport.Table />
    </DataReport.Root>
  );
}

payload is validated with the contract's reportPayloadSchema; an invalid payload renders nothing and calls onInvalid. text is the placeholder source ({{dp:id}} / {{lit:text}}) — when provided, DataReport.Prose renders it with clickable value pills (onPillClick on the root receives the datapoint id). When the prop is absent, the root falls back to the payload's own optional text field, so a persisted payload that carries its source renders prose and charts without the caller threading the text through.

DataReport.Prose renders a markdown subset so a report can be structured: paragraphs (blank-line separated, soft breaks kept), bullet lists (- / * ), GFM tables (header, |---| separator, rows), **bold** and *italic*. A value pill works anywhere text does, including inside a table cell. Out of scope on purpose: column alignment colons are accepted but not carried through, pipes cannot be escaped, and anything else renders as plain text. The root is a <div data-report-prose> (previously a <p>; a breaking change for consumers selecting on the tag); paragraphs are <p data-prose-paragraph>, lists <ul data-prose-list>, tables <table data-prose-table>.

Charts

A report puts series datapoints (value is an ordered list of { label, value } points) on a chart with a fenced block the gateway has already validated:

```chart
kind: line
title: Monthly MTU
series:
  - {{dp:custom_mtu_monthly_1a2b3c4d}}
```

DataReport.Prose lifts the block out of the prose and renders it with Chart (or StatTiles for kind: stat), looking each id up in the payload. Points align by index and the first series supplies the category labels. A block that fails to parse renders as plain prose rather than disappearing. A series id the payload lacks, or that resolves to a scalar datapoint, is not drawn: the <figure> carries data-missing with the space-separated ids and the legend keeps an entry per missing id marked data-missing (no swatch, no data-series-index), the same hook [data-value-pill][data-missing] offers. Outside a chart, a series placeholder renders its formatted summary as an ordinary pill, and the audit panel for a series row lists every point under <ol data-series-points>.

Kinds and options

| Block | Rendering | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | kind: line | one [data-series-line] path and [data-point] markers per series, overlaid | | kind: bar | one rect[data-point] per point per series, grouped per category from a zero baseline | | kind: area | line plus a path[data-area] polygon closed along the baseline | | kind: stat | StatTiles: a [data-stat-grid] of [data-stat-tile] KPI tiles instead of an svg | | stacked: true | bars share one slot and areas nest; each series is offset by the cumulative total of the series listed before it, and the domain covers the stacked totals | | orientation: horizontal | bars grow rightward from the zero baseline, categories run down the y axis, value ticks along the x axis | | layout: multiples | one [data-chart-panel] per series in a [data-chart-panels] wrapper, each with its own svg, domain and unit; a [data-panel-title] replaces the legend | | target: {{dp:id}} | a [data-target] reference line at the scalar value (it extends the domain), with the formatted value as an end label and a [data-legend-target] entry |

Figure attributes mirror the spec: data-chart-kind, data-chart-layout, data-chart-stacked and data-chart-orientation are present when set.

Chart and StatTiles are also exported on their own:

import { Chart, StatTiles } from '@exodus/datapoints-react';

<Chart
  spec={{ kind: 'bar', title: 'Revenue', stacked: true, series: [revenue.id, cost.id] }}
  series={[
    { id: revenue.id, unit: revenue.unit, points: revenue.value },
    { id: cost.id, unit: cost.unit, points: cost.value },
  ]}
  target={{ id: goal.id, value: goal.value, formatted: goal.formatted }}
  onLegendClick={(id) => openAudit(id)}
/>;

<StatTiles
  title="Headline"
  entries={[{ id: mtu.id, datapoint: mtu }]}
  onTileClick={(id) => openAudit(id)}
/>;

The SVG is hand-drawn with no dependencies and no colors or sizes of its own: it carries a viewBox (640×240) and scales to whatever width the consumer gives it. Each series' stroke/fill is var(--chart-series-N, currentColor) (N from 1), so a consumer sets up to eight series colors once:

[data-chart] {
  --chart-series-1: var(--accent);
  --chart-series-2: teal;
}
[data-chart] svg {
  width: 100%;
  height: auto;
}
[data-chart-kind='line'] [data-series-line],
[data-chart-kind='area'] [data-series-line] {
  stroke-width: 2;
}
[data-chart-kind='area'] [data-area] {
  fill-opacity: 0.18;
}
[data-target] line {
  stroke: gray;
  stroke-dasharray: 4 3;
}
[data-chart-panels] {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
[data-stat-grid] {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
}
[data-sparkline] {
  width: 6rem;
  height: 2rem;
  color: var(--chart-series-1);
}
[data-axis] text {
  font-size: 10px;
  fill: gray;
}

Vocabulary: <figure data-chart data-chart-kind> wraps <figcaption data-chart-title>, a <div data-plot> holding the <svg role="img"> (labelled by the title or the series ids) and, while a point is hovered, the tooltip, and <ul data-chart-legend>. Inside the svg: <g data-axis="y"> and <g data-axis="x"> hold [data-tick] entries; value ticks carry data-value, a [data-gridline] and the value formatted with the series unit, category labels carry data-label and are thinned to at most twelve on the x axis. Which axis carries values follows the orientation. Each series is <g data-series data-series-index data-datapoint-id> with a [data-series-line] path on line and area charts, a [data-area] polygon on area charts, and one [data-point data-label data-value] marker or bar per point, each with a <title> ("label · formatted value"; a stacked shape reports its own value, not the running total). <g data-target data-value data-datapoint-id> holds the reference line and its <text> label. Legend entries are <button data-legend-item data-datapoint-id data-series-index> with a [data-legend-swatch] and the datapoint id without its hash (data-missing in place of the index and swatch, or data-legend-target for the target); clicking one calls the root's onPillClick, so the same audit flow opens for a chart as for a pill.

Interaction

Charts answer the cursor and the keyboard without a dependency, and expose every state as data attributes so the consumer decides what it looks like:

  • A transparent <rect data-hover-overlay> covers the plot. Moving or pressing a pointer over it snaps to the nearest point (line and area by point spacing, bars by slot; horizontal bars read the row from y). The <svg> and, for a single plot, the figure carry data-hover-index; each [data-chart-panel] of a multiples chart hovers on its own.
  • While hovered the svg draws <line data-crosshair> through the point (vertical, or horizontal for horizontal bars) and every marker or bar at that index gets data-hovered.
  • A <div data-tooltip role="status" aria-live="polite"> follows the svg inside [data-plot], referenced by the svg's aria-describedby. It holds [data-tooltip-label] (the point label) and one <div data-tooltip-row data-series-index data-datapoint-id> per series with a [data-legend-swatch], [data-tooltip-name] (the id without its hash) and [data-tooltip-value] (the point formatted with the series unit); a target adds a row with data-tooltip-target. The tooltip's only inline style is two custom properties, --tooltip-x and --tooltip-y, the anchor as fractions of the svg box (the point x and the plot top; the plot middle and the row y for horizontal bars), so CSS positions it relative to [data-plot].
  • The svg is focusable (tabindex="0"): ArrowLeft/ArrowRight step the index, Home/End jump, Escape clears, and losing focus clears. Pointer leave clears.
  • Hovering or focusing a legend entry sets data-highlight-series on the figure and data-dimmed on every other [data-series]; leaving or blurring clears it. Clicking still calls onPillClick.

Stat tiles have no hover state; the sparkline is decoration for the value.

[data-plot] {
  position: relative;
}
[data-hover-overlay] {
  cursor: crosshair;
}
[data-crosshair] {
  stroke: gray;
  stroke-dasharray: 2 2;
}
circle[data-point][data-hovered] {
  r: 5;
}
[data-series][data-dimmed] {
  opacity: 0.3;
  transition: opacity 150ms;
}
[data-tooltip] {
  position: absolute;
  top: calc(var(--tooltip-y) * 100%);
  left: clamp(6rem, var(--tooltip-x) * 100%, calc(100% - 6rem));
  transform: translateX(-50%);
  pointer-events: none;
}

Stat tiles: <div data-chart data-chart-kind="stat" data-stat-grid> holds an optional [data-chart-title] and one <article data-stat-tile data-datapoint-id> per entry wrapping a <button data-stat-open> with [data-stat-label] (the datapoint description, else its id without the hash) and [data-stat-value] (a scalar's formatted, or a series' last value formatted with its unit). A series tile adds <svg data-sparkline> (a [data-series-line] path and a [data-sparkline-last] marker, no axes) and [data-stat-span] with the first and last labels. A missing id renders the tile with data-missing and no value. Tiles never derive numbers the payload does not carry.

The first cell of every expandable row carries a [data-row-caret] glyph ( collapsed, expanded) so the affordance exists unstyled; hide or restyle it through the attribute.

The datapoint cell carries a ReviewMark: an inline SVG (shield for human-reviewed, warning triangle for ai-reviewed) with the explanation in its title, so it reads correctly with no CSS; style it through [data-review-mark][data-tier].

Rows expand on click into an audit panel rendered as a <dl data-audit-content> of <dt data-audit-label> / <dd data-audit-value> pairs, so it is legible unstyled: description, freshness ("valid for 5m · retrieved 4:02 PM"), the SQL query or formula, the review tier (with the reviewer's reasons for custom queries and a data-ai-reviewed hook on every custom datapoint), and the formula with its inputs for derived values.

Collapsed regions stay mounted so they can animate. That means the audit content (query, formula, verdict) is always present in the rendered HTML; hidden and inert are presentation, not access control, so do not rely on the collapsed state to keep audit detail out of the DOM. Audit rows carry [data-collapsible], get hidden + inert while collapsed and data-expanded while open, and aria-controls always resolves. The collapsible's only child is an unstyled <div> wrapping the audit content, so a consumer can clip it without fighting the content's padding or border. Unstyled, hidden keeps them out of the way; to animate, override [hidden] and transition the wrapper:

[data-audit-row][hidden] {
  display: table-row;
}
[data-collapsible] {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 200ms;
}
[data-collapsible] > * {
  overflow: hidden;
  min-height: 0;
}
[data-collapsible][data-expanded] {
  grid-template-rows: 1fr;
}

onExpandedChange(expandedIds, changed) receives { datapointId, expanded } for the row that toggled, so a consumer can scroll the right region into view.

Styling

Everything is addressable through data attributes:

[data-report-table] tr[data-tier='ai-reviewed'] {
  /* caution tint */
}
[data-report-table] tr[data-stale] [data-retrieved] {
  color: orange;
}
[data-value-pill] {
  font-variant-numeric: tabular-nums;
}
tr[data-expanded] {
  background: color-mix(in srgb, currentColor 6%, transparent);
}

Staleness is computed client-side on a live interval from each datapoint's retrievedAt and freshness; a value past its freshness window gets data-stale.

Columns

<DataReport.Table
  columns={[
    { key: 'datapoint' },
    { key: 'value', render: (datapoint) => <strong>{datapoint.formatted}</strong> },
    { key: 'custom', id: 'unit', header: 'Unit', render: (datapoint) => datapoint.unit },
  ]}
/>

Built-in columns: datapoint, value, type, source, retrieved (that order is the default). A built-in column's render overrides its default cell; custom columns add new ones.

Reference

| Export | Purpose | | ------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | DataReport.Root | Validation, context, expansion state (expandedIds/onExpandedChange for controlled mode) | | DataReport.Prose | Placeholder text with value pills | | DataReport.Table / .Row / .AuditPanel | The verify table | | Chart | Headless SVG line/bar/area chart over series datapoints (ChartProps, ChartSeries, ChartTarget) | | StatTiles | Headless KPI tiles for scalar and series datapoints (StatTilesProps, StatEntry) | | ReviewMark, REVIEW_MARK_TITLES | Tier icon with tooltip text, for custom cells | | useRelativeTime | Live relative/absolute time + staleness for custom cells | | DEFAULT_COLUMNS, ColumnConfig | Column configuration |