A React-based widget for displaying publication metrics from multiple platforms. Designed for publishers to easily embed comprehensive usage statistics for their publications identified by DOI.
- 📊 Multiple Visualization Tabs: Measures, Timeline, Map, Regions, and Countries
- 🗺️ Geographic Distribution: Interactive world map showing global reach
- 📈 Time-based Analytics: Track metrics over time with customizable date ranges
- 🎨 Fully Customizable: Override styles via CSS custom properties or a typed
themeprop - 📱 Responsive Design: Works seamlessly on mobile, tablet, and desktop
- 🔌 Easy Integration: Works with React or JavaScript projects
npm install metrics-widgetReact 19 users may see a peer dependency warning from react-simple-maps, whose published peer range has not yet been updated for React 19. The widget is intended for React 19 and installs/builds with React 19 despite that upstream warning.
npm install metrics-widget<!-- CSS -->
<link rel="stylesheet" href="https://unpkg.com/metrics-widget@2/dist/metrics-widget.css">
<!-- Import map for React dependencies -->
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@19",
"react-dom": "https://esm.sh/react-dom@19",
"react-dom/client": "https://esm.sh/react-dom@19/client",
"react/jsx-runtime": "https://esm.sh/react@19/jsx-runtime"
}
}
</script>
<!-- Widget script -->
<script type="module">
import { initMetricsWidget } from 'https://unpkg.com/metrics-widget@2/dist/metrics-widget.js';
// Use widget here
</script>import { MetricsWidget } from 'metrics-widget';
import 'metrics-widget/styles.css';
function App() {
return (
<div>
<h1>Publication Metrics</h1>
<MetricsWidget doi="10.11647/OBP.0159" />
</div>
);
}
export default App;<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Metrics Widget Demo</title>
</head>
<body>
<div id="metrics-container"></div>
<script type="module">
import 'metrics-widget/styles.css';
import { initMetricsWidget } from 'metrics-widget';
// Initialize the widget
const widget = initMetricsWidget('metrics-container', '10.11647/OBP.0159');
// Cleanup when needed
// widget.unmount();
</script>
</body>
</html>For environments without a build tool, use import maps to load React from CDN:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Metrics Widget Demo</title>
<!-- Load the widget CSS -->
<link rel="stylesheet" href="https://unpkg.com/metrics-widget@2/dist/metrics-widget.css">
<!-- Import map for React dependencies from CDN -->
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@19",
"react-dom": "https://esm.sh/react-dom@19",
"react-dom/client": "https://esm.sh/react-dom@19/client",
"react/jsx-runtime": "https://esm.sh/react@19/jsx-runtime"
}
}
</script>
</head>
<body>
<h1>Publication Metrics</h1>
<div id="app"></div>
<!-- Initialize widget using ES modules -->
<script type="module">
import { initMetricsWidget } from 'https://unpkg.com/metrics-widget@2/dist/metrics-widget.js';
// Initialize the widget
const widget = initMetricsWidget('app', 'https://doi.org/10.11647/OBP.0159');
console.log('Widget initialized:', widget);
// Cleanup when needed
// widget.unmount();
</script>
</body>
</html>Note: Import maps are supported in all modern browsers (Chrome 89+, Firefox 108+, Safari 16.4+, Edge 89+). For older browsers, use a bundler or polyfill.
interface MetricsWidgetProps {
doi: string; // DOI identifier (with or without https://doi.org/ prefix)
theme?: MetricsWidgetTheme | null; // Optional runtime token overrides (see "Customization")
}MetricsWidgetTheme is a typed Partial<Record<MetricsWidgetTokenName, string>> exported from the package; consumers get autocomplete for every mw-* token name and rejection of typos at compile time.
Example DOI formats:
"10.11647/OBP.0159""https://doi.org/10.11647/OBP.0159"
The component also self-wraps its required React Query / services / theming providers — render <MetricsWidget> directly without composing any extra context yourself.
Initializes the metrics widget in the specified container.
Parameters:
containerId(string): The ID of the DOM element to render the widget indoi(string): The DOI identifier for the publicationoptions(object, optional):theme?: MetricsWidgetTheme— initial theme (same shape as the Reactthemeprop)
Returns:
interface MetricsWidgetInstance {
unmount: () => void; // remove the widget from the DOM
setTheme: (theme: MetricsWidgetTheme | undefined) => void; // re-theme without remounting
}Example:
const widget = initMetricsWidget('app', '10.11647/OBP.0159', {
theme: {
'mw-color-background': '#1a1a1a',
'mw-color-typography': '#f5f5f5',
},
});
// Re-theme on the fly
widget.setTheme({ 'mw-color-background': '#fff' });
// Tear it down
widget.unmount();The published widget connects to these default APIs:
VITE_THOTH_API_URL:https://api.thoth.pub/graphql(publication metadata)VITE_METRICS_API_URL:https://metrics-api.operas-eu.org(usage metrics)
Endpoint overrides are not currently exposed as part of the public package API. The VITE_* names are used only when building this repository from source.
The widget exposes its design tokens as CSS custom properties — every visible color and layout dimension is namespaced under --mw-* so the bundle never collides with the host page. There are two ways to override them; pick whichever fits your stack.
- Token namespace. Every overridable token is prefixed
--mw-*; you can't accidentally clobber a host token of the same name. - Cascade layer. Bundled styles live inside
@layer metrics-widget, so any consumer rule outside any layer wins regardless of source order.
Drop a :root block into any stylesheet. The widget's CSS lives inside @layer metrics-widget, so anything you write outside a layer wins automatically — no source-order gymnastics required.
:root {
/* Backgrounds & typography */
--mw-color-background: #fff9e6;
--mw-color-background-alt: #fffbf0;
--mw-color-typography: #1a1a1a;
/* Accents */
--mw-color-active: hsl(284, 36%, 59%);
--mw-color-border: hsl(284, 26%, 41%);
--mw-ring: hsl(276, 26%, 50%);
/* Per-platform chart colors */
--mw-chart-metric-google-books: #ff8800;
--mw-chart-metric-twitter: #1da1f2;
/* Countries/Regions pie palette (darkest → lightest) */
--mw-color-countries-1: hsl(284, 54%, 14%);
--mw-color-countries-2: hsl(284, 36%, 28%);
--mw-color-countries-3: hsl(284, 26%, 41%);
/* ...through --mw-color-countries-11 */
}Caveat. Cascade layers don't beat host
!important. If another stylesheet already uses!importanton the same token, your override will need!importanttoo.
Pass a JS object to <MetricsWidget> or initMetricsWidget. Useful when the parent app drives theming from state — for example a "brand mode" toggle. The keys are the token names without the leading --, and the type is exported as MetricsWidgetTheme for autocomplete and typo-protection.
import {
MetricsWidget,
type MetricsWidgetTheme,
} from 'metrics-widget';
import 'metrics-widget/styles.css';
const brandTheme: MetricsWidgetTheme = {
'mw-color-background': '#1a1a1a',
'mw-color-typography': '#f5f5f5',
'mw-color-active': 'hsl(280, 50%, 60%)',
'mw-chart-metric-google-books': '#ff8800',
};
export default function App() {
return <MetricsWidget doi="10.11647/OBP.0067" theme={brandTheme} />;
}Vanilla JS:
import { initMetricsWidget } from 'metrics-widget';
const widget = initMetricsWidget('metrics-container', '10.11647/OBP.0067', {
theme: {
'mw-color-background': '#1a1a1a',
'mw-color-typography': '#f5f5f5',
},
});
// Re-theme without remounting
widget.setTheme({ 'mw-color-background': '#fff' });
// Tear it down
widget.unmount();The CSS-variable path and the JS-prop path can be combined: pass a theme prop for runtime/state-driven values, and use a :root block for static brand defaults.
A few rules govern how the theme prop is applied at runtime:
- Merge, not replace. Each new
themeobject merges into the previously applied set. Keys you omit from a subsequent update keep their previously applied values — they are not auto-reverted. To clear a single key, passundefinedfor it (theme={{ 'mw-color-background': undefined }}); to clear everything, unmount the widget. - Color values are validated. Color tokens (everything except layout dimensions,
mw-radius, andmw-tooltip-drop-shadow) are validated before being applied. Accepted formats: named CSS colors (red,rebeccapurple), 3- or 6-digit hex (#fff,#1a1a1a),rgb()/rgba(), andhsl()/hsla(). Other formats — 4- or 8-digit hex (#abcd,#1a1a1a80),oklch(),color(),lab(),lch()— are not currently validated and will be rejected. Invalid values are silently dropped: the previously applied valid value (or bundle default) stays in place. - Non-color tokens are not validated. Layout, radius, and shadow tokens accept any string and are written through as-is.
- Cleanup on unmount. Every key the widget ever wrote via the prop is removed from
document.documentElement.stylewhen the widget unmounts, so no overrides leak back to the host. - Hidden until applied. On first mount the widget keeps its root
visibility: hiddenuntil tokens have been written todocument.documentElement. The reveal happens before the first paint, so consumers passing a synchronousthemenever see a flash of bundle defaults.
The hide-until-applied gate releases on the first commit after mount. If your theme is fetched asynchronously (API, localStorage, matchMedia, …), mount the widget conditionally so its first commit already carries the resolved theme:
const theme = useResolvedTheme(); // returns undefined until ready
return theme ? <MetricsWidget doi={doi} theme={theme} /> : null;Mounting with theme={undefined} and updating to theme={resolved} later will release the gate on the first commit with bundle defaults, then repaint when your theme arrives.
All overridable tokens, grouped by tier:
# Semantic
--mw-color-background, --mw-color-background-active, --mw-color-background-alt
--mw-color-typography, --mw-color-typography-alt
--mw-color-border, --mw-color-active, --mw-ring
# Components
--mw-color-nav-background, --mw-color-nav-background-active, --mw-color-nav-typography
--mw-color-header-background
--mw-color-button-background, --mw-color-button-typography, --mw-color-button-border
--mw-color-button-typography-hover, --mw-color-button-border-hover
--mw-color-button-typography-active, --mw-color-button-border-active
--mw-color-button-active-background, --mw-color-button-active-typography, --mw-color-button-active-border
--mw-color-input-background, --mw-color-input-border, --mw-color-input-typography
--mw-color-select-background-selected, --mw-color-input-start-icon
--mw-color-tooltip-background, --mw-tooltip-drop-shadow
--mw-color-divider, --mw-color-spinner
--mw-chart-legend-background
--mw-color-placeholder-icon, --mw-color-placeholder-icon-bg
# shadcn-derived (ui primitives)
--mw-radius
--mw-background, --mw-foreground
--mw-popover, --mw-popover-foreground
--mw-primary, --mw-primary-foreground
--mw-secondary, --mw-secondary-foreground
--mw-muted, --mw-muted-foreground
--mw-destructive, --mw-border
# Layout
--mw-max-width, --mw-max-height
--mw-header-height, --mw-footer-height, --mw-content-height
# Countries/Regions pie palette
--mw-color-countries-1 ... --mw-color-countries-11
# Per-platform chart palette
--mw-chart-metric-default
--mw-chart-metric-the-classics-library, --mw-chart-metric-google-books
--mw-chart-metric-open-book-publishers
--mw-chart-metric-open-book-publishers-html-reader
--mw-chart-metric-open-book-publishers-pdf-reader
--mw-chart-metric-open-edition, --mw-chart-metric-oapen
--mw-chart-metric-twitter, --mw-chart-metric-wikimedia, --mw-chart-metric-wikipedia
--mw-chart-metric-wordpress-com, --mw-chart-metric-world-reader
--mw-chart-metric-crossref
# Map gradient
--mw-chart-map-zero, --mw-chart-map-lowest, --mw-chart-map-highest
The widget provides five comprehensive tabs for analyzing publication metrics:
Displays overall metrics broken down by:
- Platform: Usage across different platforms (OAPEN, OpenEdition, JSTOR, etc.)
- Book-level metrics: Total views, downloads, and sessions
- Chapter-level metrics: Individual chapter performance with filtering options
Features:
- Filter by specific platforms
- Search and filter chapters
- Download data as CSV
- Interactive pie charts with detailed breakdowns
Visualizes metrics over time:
- Monthly aggregated data
- Multiple metrics displayed simultaneously
- Year-based pagination for large datasets
- Interactive legend to toggle metric visibility
Perfect for:
- Tracking growth trends
- Identifying seasonal patterns
- Comparing platform performance over time
Interactive world map visualization:
- Geographic distribution of usage
- Color-coded countries by metric intensity
- Hover tooltips with detailed country statistics
- Zoom and pan functionality
Helps identify:
- Geographic reach of publications
- Regional popularity
- International adoption
Continental-level breakdown:
- Pie chart visualization of regional distribution
- Percentage-based comparisons
- Sortable region list
- CSV export capability
Regions include:
- Africa, Americas, Asia, Europe, Oceania
- Antarctic (if applicable)
Detailed country-level analytics:
- Top countries ranked by usage
- Percentage distribution
- Interactive pie chart
- Sortable country list with percentages
- CSV download option
Shows top 10 countries by default (configurable).
graph TB
Publisher[Publisher Website] --> Widget[Metrics Widget]
Widget --> Providers[React Context Providers]
Providers --> Query[React Query Client]
Query --> ThothAPI[Thoth API GraphQL]
Query --> MetricsAPI[Metrics API REST]
ThothAPI --> MetaData[Publication Metadata]
MetricsAPI --> MetricsData[Usage Metrics]
MetaData --> Tabs[Widget Tabs]
MetricsData --> Tabs
Tabs --> Measures[Measures Tab]
Tabs --> Timeline[Timeline Tab]
Tabs --> Map[Map Tab]
Tabs --> Regions[Regions Tab]
Tabs --> Countries[Countries Tab]
Measures --> Charts[Recharts Visualizations]
Timeline --> Charts
Map --> MapLib[react-simple-maps]
Regions --> Charts
Countries --> Charts
- Initialization: Widget receives DOI prop
- Validation: DOI is validated using
doi-utilslibrary - Metadata Fetch: Publication metadata retrieved from Thoth API
- Metrics Fetch: Usage metrics fetched from Metrics API
- Processing: Data is transformed and aggregated
- Visualization: Charts and maps render the processed data
The widget tracks metrics from the following platforms:
- Google Books
- Open Book Publishers (HTML/PDF readers)
- OpenEdition
- OAPEN
- World Reader
- JSTOR
- The Classics Library
- Unglue.it
- OpenAIRE
- Wikimedia/Wikipedia
- Figshare
- SUB Göttingen
- EKT (National Documentation Centre)
- UPLO
- WordPress.com references
- Node.js 18+ and npm
- React 19+ (peer dependency)
# Clone the repository
git clone https://github.com/thoth-pub/metrics-widget.git
cd metrics-widget
# Install dependencies
npm install
# Create environment file
cp .env.example .env
# Start development server
npm run dev# Production build
npm run build
# Preview build
npm run preview