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

@ovalbee/reportifact

v0.0.5

Published

Vue 3 component + Express middleware for reports from Parquet files via DuckDB

Readme

Reportifact

Vue report renderer for report assets. A report is a directory with an index.md plus optional JSON, Parquet, Markdown, TensorBoard event, image, or other files referenced from that index.

Widget Index

| Widget | Use For | | --- | --- | | grid | Group widgets with semantic layouts like auto, 2, 4-2. | | card | Bordered Markdown panel with optional title, subtitle, and badge. | | notice | Lightweight placeholder, TODO, or informational callout. | | metricCards | Compact metric tiles from a Markdown table. | | metricRows | Metric rows with values, descriptions, and help tooltips. | | metricCallout | One highlighted metric plus explanatory Markdown. | | imageGallery | JSON-backed grid of images with optional overlay layers. | | modelComparisonGallery | Model-comparison image cards that open per-item previews. | | chart | JSON-backed ECharts visualizations. | | modelClassComparison | Compact comparison of per-class metrics across ML models. | | table | Parquet table with filtering, sorting, pagination, and row clicks. | | timeline | JSON-backed per-row time tracks with point markers (e.g. videos with cut points). | | markdown | Render a separate Markdown file. | | tensorboard | TensorBoard scalar event charts. | | metricsTable | JSON-backed metrics/table visualization. | | scope | Sticky control to re-render the whole report for a data scope (asset-tag value). | | evaluationReports | OvalBee-only custom widget for evaluation reports linked to a model asset. | | modelCheckpoints | OvalBee-only custom widget for model checkpoint downloads. |

Built-in widgets are provided by Reportifact. OvalBee-only widgets must be registered by the OvalBee host through customWidgets.

OvalBee Custom Widgets

| Widget | Use For | Host Component | | --- | --- | --- | | evaluationReports | Evaluation report links for a model asset. | web/src/components/reportifact/custom-widgets/EvaluationReportsWidget.vue | | modelCheckpoints | Download links for checkpoint resources on a model asset. | web/src/components/reportifact/custom-widgets/ModelCheckpointsWidget.vue |

index.md Shape

Top-level # H1 headings become report sections and table-of-contents tabs. Inside a section, use Markdown prose and ::widget directives.

# Overview

Plain Markdown text.

::chart
src: metrics.json
type: radar
title: Metrics
::

Do not put raw HTML, inline CSS, or <style> blocks in report Markdown. Add a Reportifact widget instead. Markdown is supported inside widgets that accept a body, such as card, notice, metricRows, and metricCards.

Directive Syntax

Two forms are supported:

::widget
key: value
nested:
  key: value
::
::widget{title="Short title" layout="4-2"}
Markdown body here.
::

Values are parsed as strings, numbers, or booleans.

Layout

Use ::grid to group widgets. Prefer semantic layouts over CSS strings.

::grid{layout="4-2"}
::card{title="A"}...::
::card{title="B"}...::
::card{title="C"}...::
::card{title="D"}...::
::

Layouts:

| Layout | Behavior | | --- | --- | | auto | Default. Uses child count to choose a reasonable layout. | | 1, 2, 3, 4 | Fixed column intent; collapses on narrow containers. | | 4-2 | Four columns when wide, two columns when narrow. Good for four cards. | | 6-3-2 | Six columns wide, three medium, two narrow. |

Grid items stretch to the same row height.

A grid cell may itself be a ::grid (one level of nesting). Use this to put a single widget in one column and a vertical stack of widgets in the next:

::grid{layout="2"}
::chart
src: left.json
type: bar-gradient
::
::grid{layout="1"}
::chart
src: right_a.json
type: bar-gradient
::
::chart
src: right_b.json
type: bar-gradient
::
::
::

Widgets

card

Generic bordered panel. Body is Markdown, including compact Markdown tables.

::card{title="Model" surface="subtle"}
**Framework:** DEIM

**Checkpoint:** best.pth
::

Attrs:

| Attr | Values | | --- | --- | | title | Header title. | | subtitle | Small header subtitle. | | badge | Pill text on the right. | | badgeValue | Number used to color the badge. | | surface | default, subtle, dashed. |

notice

Lightweight callout or placeholder.

::notice{title="Prediction gallery placeholder" variant="dashed"}
TODO: add annotated previews.
::

Attrs: title, variant (default, dashed).

metricCards

Compact metric tiles from a Markdown table.

::metricCards
| Label | Value | Description |
| --- | ---: | --- |
| mAP | 0.2314 | COCO-style quality. |
| F1 | 0.3245 | Balance of precision and recall. |
::

Columns: Label, Value, Description.

metricRows

Metric rows with optional help tooltips.

::metricRows{title="Detection Metrics" subtitle="Summary values."}
| Metric | Value | Help | Description |
| --- | ---: | --- | --- |
| Precision | 0.5385 | Correct detections / detections. | Correct predictions. |
::

Attrs: title, subtitle. Columns: Metric, Value, Help, Description.

metricCallout

Single highlighted metric plus Markdown body.

::metricCallout{label="Mean IoU" value="0.7782" description="Average overlap quality."}
Use this together with AP75 to inspect localization.
::

Attrs: label, value, description.

imageGallery

Renders a JSON-backed grid of images. Each tile has a required image layer and an optional overlay layer.

::imageGallery
src: image_gallery.json
overlayOpacity: 0.7
::
{
  "assets": ["image-asset-id", "image-asset-id"],
  "annotations": ["gt-annotation-id", "prediction-annotation-id"]
}

For static report files, use images and overlays instead:

{
  "images": ["gallery/image.jpg"],
  "overlays": ["gallery/overlay.png"]
}

Attrs:

| Attr | Default | | --- | --- | | src | Required JSON file. | | overlayOpacity | 0.7 | | fit | contain; use cover for cropped tiles. | | aspectRatio | 4 / 3 | | drawTags | true; set to false to render annotation overlays without tag labels (useful for object-detection galleries). |

The gallery renders four columns by default and two columns on narrow containers. Inline preview shows the first row; when there are more asset-backed items, Reportifact emits an asset-preview row-click payload for the host to open a larger viewer.

Static URLs and report-local files work directly. assets and annotations are host-specific refs; pass a mediaResolver prop to resolve them. Reportifact does not know about OvalBee assets.

modelComparisonGallery

Renders comparison cards backed by host-resolved image assets. Each card can include a ground-truth overlay, a short list of details, and an asset-preview payload that opens when the card is activated.

::modelComparisonGallery
src: image_comparison.json
columns: 3
previewLimit: 3
fit: cover
drawTags: false
::
{
  "items": [
    {
      "asset": "image-asset-id",
      "annotation": "gt-annotation-id",
      "label": "street_0142.jpg",
      "details": [
        { "label": "F1 spread", "value": "0.3100" },
        { "label": "Mean F1", "value": "0.6400" }
      ],
      "compare": {
        "action": "asset-preview",
        "layout": "viewer",
        "mode": "side",
        "sync": true,
        "assets": [
          {
            "asset": "image-asset-id",
            "layers": [{ "annotation": "gt-annotation-id", "reference": true }]
          },
          {
            "asset": "image-asset-id",
            "layers": [{ "annotation": "prediction-annotation-id" }]
          }
        ]
      }
    }
  ]
}

Attrs:

| Attr | Default | | --- | --- | | src | Required JSON file. | | overlayOpacity | 0.7 | | fit | contain; use cover for cropped cards. | | aspectRatio | 4 / 3 | | drawTags | true | | columns | 4; clamped to 16. | | previewLimit | 4; clamped to 124. |

Only visible cards resolve thumbnails. Activating a card emits its compare payload. Show all emits a collection with click: "comparison", preserving each item's nested comparison payload. Missing annotations render the base image without an overlay.

chart

Renders a JSON chart file with ECharts.

::chart
src: key_metrics.json
type: radar
title: Key Metrics
height: 420
::

Attrs:

| Attr | Values | | --- | --- | | src | JSON file in the report directory. | | type | pie, line, radar, bar-normalized, bar-gradient, bar-grouped, bar-stacked, heatmap, custom-gauge, gauge-grade, range, scatter. | | title | Chart title override. | | height | CSS length or number in px. | | radius | For pie; a single value ("50%") renders a solid pie, a comma-separated pair ("46%,70%") renders a donut ring. | | smooth | For line-like charts; false disables smoothing. | | horizontal | For bar-gradient; true lays the bars out horizontally (categories on the y-axis). |

Charts are full-width by default.

Chart data can attach host actions with explicit click IDs. Put clickId on the rendered data point and map it to an inline payload or report-local JSON file in click.payloads.

{
  "xAxis": ["horse"],
  "series": [
    {
      "name": "FP",
      "data": [{ "value": 42, "clickId": "outcome.fp.horse" }]
    }
  ],
  "click": {
    "version": 2,
    "payloads": {
      "outcome.fp.horse": { "src": "outcome_fp_horse_click.json" }
    }
  }
}

Pie charts may use the same click contract by wrapping points in a top-level data field:

{
  "data": [{ "name": "Annotated", "value": 12, "clickId": "coverage.annotated" }],
  "click": {
    "version": 2,
    "payloads": {
      "coverage.annotated": { "payload": { "action": "asset-preview", "layout": "collection", "assets": [] } }
    }
  }
}

For external payloads, Reportifact emits { "action": "load-report-payload", "src": "..." }. The host resolves that report-local JSON file, then handles the loaded payload the same way it handles table row-click payloads.

modelClassComparison

Compares per-class metrics across two or more ML models on a shared horizontal scale. The first metric is active initially, and a compact switcher selects another metric.

::modelClassComparison
src: per_class_matrix.json
::
{
  "min": 0,
  "max": 1,
  "models": [
    { "id": "report-a", "name": "Detector A", "color": "#0072B2" },
    { "id": "report-b", "name": "Detector B", "color": "#E69F00" }
  ],
  "metrics": [
    { "key": "ap", "label": "AP" },
    { "key": "f1", "label": "F1" }
  ],
  "classes": [
    {
      "name": "car",
      "values": {
        "report-a": { "ap": 0.82, "f1": 0.78 },
        "report-b": { "ap": 0.76, "f1": 0.74 }
      }
    }
  ]
}

Model IDs and metric keys must be unique. Colors must be six-digit hex values. Models use circular markers distinguished by color. Overlapping markers collapse into an accessible cluster that exposes each exact value on hover, focus, or tap. With up to eight classes the full matrix is shown. Larger matrices add search and ten-row pagination. Rows are sorted alphabetically, and malformed rows without a comparable metric are ignored.

table

Server-backed Parquet table with filtering, sorting, pagination, and optional row clicks.

::table
src: per_image_metrics.parquet
pageSize: 20
fixFirstColumn: true
::

Attrs:

| Attr | Default | | --- | --- | | src | Required Parquet file. | | pageSize | 10 | | fixFirstColumn | true | | hideHeader | false | | columnTooltips | Optional map of column name to tooltip text. |

Row clicks: include a $row_click column in the Parquet row. The value should be a JSON string or object. Reportifact emits it through row-click. Columns whose names start with $ are treated as system columns and are hidden from the rendered table.

Common OvalBee payload — the single host contract is action: "asset-preview" (see .agents/reports-and-preview.md for the full contract). It expresses a direct single-asset preview, a synchronized side-by-side comparison, and a browsable collection grid in one shape via assets[] and per-asset layers[]:

{
  "action": "asset-preview",
  "layout": "viewer",
  "mode": "side",
  "sync": true,
  "assets": [
    { "asset": "asset-id-1", "label": "Ground Truth", "layers": [{ "annotation": "gt-ann-id", "reference": true }] },
    { "asset": "asset-id-2", "label": "Prediction", "layers": [{ "mask": "mask-id-1" }] }
  ]
}

A layer's overlay comes from annotation (server-rendered from an OvalBee annotation asset), mask (a ready overlay asset), or maskUrl (a ready overlay url); a layer may be missing entirely for a pane with no overlay (e.g. a negative image with no ground-truth annotation).

imageGallery emits an asset-preview collection payload when the user clicks its show-all button:

{
  "action": "asset-preview",
  "layout": "collection",
  "title": "Images",
  "click": "preview",
  "grid": 4,
  "assets": [
    { "asset": "asset-id-1", "layers": [{ "annotation": "annotation-id-1" }] },
    { "asset": "asset-id-2", "layers": [{ "annotation": "annotation-id-2" }] }
  ]
}

For large media grids or chart clicks, each tile can carry its own crop and a nested compare drill-down (a full asset-preview payload opened when that tile is clicked and click: "comparison"):

{
  "action": "asset-preview",
  "layout": "collection",
  "title": "False positives: horse",
  "click": "comparison",
  "assets": [
    {
      "asset": "image-asset-id",
      "crop": [10, 20, 120, 80],
      "layers": [{ "annotation": "prediction-annotation-id" }],
      "compare": {
        "action": "asset-preview",
        "layout": "viewer",
        "mode": "side",
        "sync": true,
        "grid": 2,
        "assets": [
          { "asset": "image-asset-id", "crop": [10, 20, 120, 80], "layers": [{ "annotation": "gt-annotation-id", "reference": true }] },
          { "asset": "image-asset-id", "crop": [10, 20, 120, 80], "layers": [{ "annotation": "prediction-annotation-id" }] }
        ]
      }
    }
  ]
}

layout: "collection" opens a browsable thumbnail grid first; layout: "viewer" opens the viewer directly. For a table row that represents one image, use a direct single-asset viewer payload:

{
  "action": "asset-preview",
  "layout": "viewer",
  "assets": [
    { "asset": "image-asset-id", "layers": [{ "annotation": "annotation-id", "label": "main / supervisely" }] }
  ]
}

Python report nodes build these payloads via the shared builder in the OvalBee SDK (ovalbee.report.asset_preview: single_asset_preview, compare_payload, media_grid_payload, asset_spec, layer) rather than hand-writing dicts.

timeline

Per-row horizontal time tracks with point markers, rendered with ECharts custom series. Built for "one bar per video with cut points" visuals, but any row/track/marker data fits.

::timeline
src: all_videos_timeline.json
title: All-videos timeline
timeFormat: clock
::
{
  "rows": [
    {
      "id": "asset-id",
      "name": "2026_01_15_Umaker_L9",
      "group": "good",
      "start": 0,
      "end": 248,
      "cuts": [62, 131, 198]
    }
  ]
}

Row fields: name and end are required; start defaults to 0; cuts are marker positions in seconds; group colors the track from a built-in palette (one legend entry per group, rows without a group share one series); color overrides the track color for that row; click is an optional payload object emitted through row-click when the row's track or a marker is clicked (same host contract as table $row_click, e.g. an asset-preview payload).

Attrs:

| Attr | Values | | --- | --- | | src | JSON file in the report directory. | | title | Chart title. | | timeFormat | Initial time-axis mode: seconds (default) or clock (mm:ss, switching to hh:mm:ss past one hour). A toolbar toggle lets the viewer change it. | | zoom | true enables x-axis zooming (inside + slider). Off by default. |

Hovering a track shows the row name, group, duration, and its cut list; hovering a marker shows that specific cut time. Widget height grows with the number of rows.

markdown

Loads and renders another Markdown file from the report directory.

::markdown
src: details.md
live: true
::

Attrs: src, live (true enables refresh).

tensorboard

Renders TensorBoard scalar event data.

::tensorboard
src: events.out.tfevents
title: Training
height: 420
smooth: true
maxPoints: 1000
::

Attrs: src, title, height, smooth, maxPoints.

metricsTable

JSON-backed metrics/table visualization. Use this when the report already emits the metrics JSON shape expected by the widget.

::metricsTable
src: metrics.json
title: Metrics
show_pct: true
::

scope

Renders a sticky "Report scoped to" breadcrumb control above the section navigation and re-renders the whole report for the selected data scope. A scope is a subset of the evaluated data identified by an asset-tag value (e.g. split.bad_weather); the base scope is all (the whole dataset).

Place the directive in the preamble (before the first # H1). It is hoisted above the table of contents and is not rendered as a section.

::scope
src: scopes.json
::

# Overview
...

src points to a manifest describing the tags, their values, and per-scope asset counts:

{
  "all": { "n": 50 },
  "tags": [
    {
      "key": "split",
      "label": "split",
      "values": [
        { "value": "bad_weather", "scopeId": "split.bad_weather", "n": 24 },
        { "value": "good", "scopeId": "split.good", "n": 26 }
      ]
    }
  ]
}

When a scope is active, the report updates in place (no remount):

  • File-backed widgets (chart, table, imageGallery, modelComparisonGallery, tensorboard, metricsTable, modelClassComparison) load a per-scope source: the suffix .<scopeId> is inserted before the extension, so outcome_counts.json becomes outcome_counts.split.bad_weather.json. The base scope uses the unsuffixed file. If a per-scope file is absent from the report, the base file is used.
  • Text (Markdown prose, card bodies, metricCards / metricRows tables, badges, titles) resolves $name / ${name} placeholders from vars.<scopeId>.json merged over the base vars.json. Authors keep placeholders in index.md (instead of substituting at build time) for any value that varies per scope; literal text stays static. Tokens with no matching key in the vars dictionary are left untouched, so non-scoped reports (already substituted, no vars.json) render unchanged.

vars.json is a flat dictionary, e.g.:

{ "map": 55.1, "ap50": 95.9, "f1": 92.4, "n_images": 50, "n_gt": 1781 }

A report without a ::scope directive (and without vars.json) behaves exactly as before.

Embedding

<script setup lang="ts">
import { Reportifact } from '@ovalbee/reportifact';
</script>

<template>
<Reportifact
  :url-map="urlMap"
  base-path="/api/reports"
  :media-resolver="resolveMedia"
  :custom-widgets="customWidgets"
  @row-click="onRowClick"
/>
</template>

Important props:

| Prop | Purpose | | --- | --- | | url | Direct URL to index.md. | | urlMap | Map file names to signed/public URLs. Uses index.md for the schema. | | linkUrlMap | Optional map for Markdown links. | | basePath | API route base for Parquet and TensorBoard proxy routes. | | scrollContainer | Scroll container used for active section tracking. | | refreshInterval | Refresh interval for live report widgets. | | mediaResolver | Optional resolver for gallery assets and annotations. | | customWidgets | Optional Vue 3 components keyed by directive name for host-owned widgets. |

Portable Widgets

Sites and other Vue hosts can render selected widgets without loading the full Markdown report runtime. Import every widget from its direct subpath; there is intentionally no runtime widgets barrel because it would pull ECharts into pages that only use metric components.

<script setup lang="ts">
import ReportifactChart from '@ovalbee/reportifact/widgets/chart';
import '@ovalbee/reportifact/style.css';

const chart = {
  xAxis: ['YOLO', 'DEIM'],
  data: [0.71, 0.76],
};
</script>

<template>
  <ReportifactChart
    type="bar-gradient"
    :data="chart"
    caption="DEIM leads on the held-out validation set."
  />
</template>

Public subpaths:

| Import | Component | Portable input | | --- | --- | --- | | @ovalbee/reportifact/widgets/host | ReportifactWidgetHost | Theme and loading-style boundary. | | @ovalbee/reportifact/widgets/chart | ReportifactChart | src JSON URL or inline data. | | @ovalbee/reportifact/widgets/metric-cards | ReportifactMetricCards | Inline data or Markdown-table content. | | @ovalbee/reportifact/widgets/metric-callout | ReportifactMetricCallout | Inline data or direct props. | | @ovalbee/reportifact/widgets/metrics-table | ReportifactMetricsTable | src JSON URL or inline data. | | @ovalbee/reportifact/widgets/timeline | ReportifactTimeline | src JSON URL or inline data. | | @ovalbee/reportifact/widgets/image-gallery | ReportifactImageGallery | src JSON URL or inline data; optional media resolver. | | @ovalbee/reportifact/widgets/contracts | Type-only contracts | Widget data and resolver types. |

For URL-backed widgets, src is the portable spelling and legacy url remains supported for the full report renderer. Inline data takes precedence when both are present. Every portable widget already renders a semantic <figure> and accepts optional caption, sourceHref, and sourceLabel props. The shared WidgetWrapper owns that semantic frame together with loading and error presentation; hosts do not add another wrapper. Static fallback images are not part of the portable v1 authoring contract.

The full report directives accept the same caption, sourceHref, and sourceLabel options. Existing reports remain valid when those options are absent.

MetricCallout's html field is trusted pre-rendered HTML. Do not pass untrusted user input without sanitizing it first.

Parquet tables, TensorBoard, scopes, and the full widget dispatcher remain full-report features. Publish a static summary or link to a public Live Example for those experiences instead of importing their internals into a content page.

Custom Widgets

Hosts can register Vue 3 components for directives that Reportifact does not own. The component is compiled into the host app bundle; the Markdown only names the widget and provides parsed options.

<script setup lang="ts">
import { Reportifact, type ReportifactCustomWidgets } from '@ovalbee/reportifact';
import ModelCheckpointsWidget from './custom-widgets/ModelCheckpointsWidget.vue';

const customWidgets: ReportifactCustomWidgets = {
  modelCheckpoints: ModelCheckpointsWidget,
};
</script>

<template>
  <Reportifact :url-map="urlMap" :custom-widgets="customWidgets" />
</template>

The registered component receives:

| Prop | Purpose | | --- | --- | | widget | Parsed widget item, including widgetType, content, and options. | | url | Resolved src URL, when the directive has one. | | options | Parsed directive options. | | html | Markdown-rendered widget body. | | context | Helpers: resolveUrl, resolveLinkUrl, mediaResolver, emitRowClick. |

Custom widgets may expose refresh(); Reportifact calls it during manual or interval refresh. Unknown unregistered directives render an unsupported-widget placeholder instead of failing the report.

Example host-owned directive:

::modelCheckpoints
assetId: 3f2504e0-4f89-11d3-9a0c-0305e82c3301
::

Development

npm run build

From this repo, prefer the containerized check:

docker compose exec web sh -lc 'cd /workspace/packages/reportifact && npm run build'