Skip to content

MeterUsageChart

Automatic query, aggregation and rendering for line, area, bar, pie, doughnut and registered custom chart types. No useEffect, loader, row mapping, pivot or manual series definitions are needed. The existing MeterChart data-only API is unchanged.

Loading interactive preview…

View codeHide code

Actual React source for this preview, including demo controls, shared styles and isolated mocks. The docs host supplies the client and theme; application installation and React / Vue usage follow below.

import { useState } from "react"
import { MeterUsageChart } from "@meterry/component-react"
import type { MeterThemeInput } from "@meterry/component"
import { METRICS, type PreviewEnvironment } from "./preview-data"
import {
styleProps,
previewSeriesRegistry,
RequestSummary,
} from "./preview-shared"
export function AutomaticChartPreview({
environment,
theme,
}: {
environment: PreviewEnvironment
theme: MeterThemeInput
}) {
const [type, setType] = useState("line")
const [limited, setLimited] = useState(false)
const [allowPartial, setAllowPartial] = useState(false)
const category = type === "pie" || type === "doughnut"
return (
<div className="docs-preview-stack" data-testid="automatic-chart-preview">
<div className="docs-preview-actions">
<label className="docs-preview-chart-select">
Automatic chart type
<select
value={type}
onChange={(event) => setType(event.target.value)}
>
{["line", "area", "bar", "pie", "doughnut"].map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
</label>
<label className="docs-preview-chart-select">
<input
type="checkbox"
checked={limited}
onChange={(event) => setLimited(event.target.checked)}
/>
Small result limit
</label>
<label className="docs-preview-chart-select">
<input
type="checkbox"
checked={allowPartial}
onChange={(event) => setAllowPartial(event.target.checked)}
/>
Allow partial chart
</label>
</div>
<MeterUsageChart
{...styleProps}
seriesRegistry={previewSeriesRegistry}
type={type}
theme={theme}
height={280}
title={
category
? "Input token share by model"
: "Daily input and output tokens"
}
limit={limited ? 3 : 500}
allowPartial={allowPartial}
query={{
start_time: environment.startTime,
end_time: environment.endTime,
metrics: category ? ["input_tokens"] : [...METRICS],
group_by: category ? ["model"] : ["bucket"],
}}
/>
<RequestSummary environment={environment} />
<div className="docs-preview-actions">
<button type="button" onClick={() => environment.failNextRequest()}>
Fail next API request
</button>
<span className="docs-preview-filter-summary">
Refresh bypasses the result cache. Errors retain the previous chart.
</span>
</div>
</div>
)
}
Terminal
npm install @meterry/component @meterry/component-react echarts echarts-for-react

For default styles, optionally install @meterry/component-tailwindcss. For authentication, shared clients and theming, see Getting Started.

Switch between line, area and bar without refetching. Pie and doughnut share the same category result. Requests and completed results are cached per shared client.

baseUrl: '' sends Subject API requests to same-origin /v1/.... Set it to your actual API origin or proxy prefix; /api/meter/subject-token is a separate application-owned endpoint, not a built-in Meter route. The preview makes no real network requests or persistent token writes.

import { MeterClientProvider, MeterUsageChart } from '@meterry/component-react'
export function UsagePage() {
return (
<MeterClientProvider tokenEndpoint="/api/meter/subject-token"
options={{ baseUrl: '', credentials: 'include', tokenStorage: null }}>
<MeterUsageChart
type="line"
query={{ metrics: ['input_tokens', 'output_tokens'], group_by: ['bucket', 'model'] }}
rangeDays={7}
/>
<MeterUsageChart
type="doughnut"
query={{ metrics: ['total_tokens'], group_by: ['model'] }}
rangeDays={7}
/>
</MeterClientProvider>
)
}

Configure the same authentication-scoped client/provider as MeterUsageTable. Explicit client takes precedence over the provider client. Token callbacks and application token endpoints remain application-owned; tenant API keys never belong in browser code.

Provide the client in an ancestor setup:

import { provideMeterClient } from '@meterry/component-vue'
provideMeterClient({ tokenEndpoint: '/api/meter/subject-token', options: { baseUrl: '', tokenStorage: null } })

In a descendant:

<script setup lang="ts">
import { MeterUsageChart } from '@meterry/component-vue'
</script>
<template>
<MeterUsageChart type="area" :query="{ metrics: ['total_tokens'], group_by: ['bucket', 'model'] }" />
<MeterUsageChart type="pie" :query="{ metrics: ['total_tokens'], group_by: ['model'] }" />
</template>

Vue component refs also expose refresh().

line, area and bar default to trend mode. The component queries analysis/query, adds bucket and metric to group_by, and generates one series for each metric/measure/non-time-dimension combination. For example, input_tokens + output_tokens grouped by model produces independent input and output series for every model.

Repeated points for the same complete series identity and same bucket are summed, never overwritten. Different models/providers/metrics are not merged. Buckets are chronologically sorted; the component zero-fills each series over the union of returned buckets. It does not synthesize entirely absent intervals itself. The current Meter backend already pads existing series across the requested window, so those returned buckets are retained; backend bucket/timezone behavior remains authoritative. Numeric epoch buckets are formatted in the requested timezone (UTC by default).

A single chart selects one additive measure (quantity by default or amount). Set measure="amount" for cost charts. If query.measures contains multiple measures, select one explicitly with measure; mixing units is rejected. Average, percentile and distinct-count measures are not summed as if they were additive. Multiple metrics stay in separate series. No cross-metric grand total is shown, so including input_tokens, output_tokens and total_tokens cannot silently produce a double-counted total.

pie and doughnut always use category mode. It queries usage/query, groups by query.group_by (default model) and generates one value series. Returned rows with the same requested category dimensions are summed, including rows split by extra response dimensions. Categories sort by descending value.

Exactly one metric and one measure are required. Ambiguous multi-metric requests are rejected instead of combining overlapping totals or unlike units. Negative category totals are rejected; zero-valued categories are omitted so an all-zero pie does not display fabricated equal shares. The current usage/query contract does not support timezone; category requests with timezone are rejected rather than silently dropping it. Explicit time boundaries still apply.

Pass a registered type and optional seriesRegistry. Custom cartesian types use trend mode; registered polar types use category mode. mode="category" can also render grouped category totals as bars. Existing chart builders and slots.chart({ model, option }) continue to work with automatically generated data.

Neither wrapped aggregate endpoint declares a cursor or definitive completeness flag. A response at the requested limit may be incomplete. Trend detection counts returned points before merging/zero filling; category detection counts returned rows before summing categories. Trend checks are conservative, including series whose time grids were filled by the backend.

Potentially truncated charts are hidden by default, with a visible notice to narrow the range or filters. This prevents a partial pie from looking like a full account-wide distribution. allowPartial explicitly permits rendering the prefix while keeping the notice. No cursor is invented and no automatic all-data export is claimed. Both flags and source counts are available on the headless result.

  • Shared client + normalized query + mode + TTL form a request identity. Default completed-result cache TTL is 30 seconds, with at most 32 retained completed entries per client. Relative time windows stay anchored to the original cached request during that TTL; refresh starts a fresh window.
  • Simultaneous identical queries share one request. An unmount/query change only aborts shared work when no other consumer still needs it.
  • Inline equivalent query objects, type changes within the same mode, theme, height, formatters and styling do not refetch.
  • Errors are not cached. Retry works without application logic; failed refreshes retain the last successful chart with a visible error. Superseded responses are ignored even when a custom fetch implementation disregards its signal.
  • cacheTimeMs={0} disables completed-result reuse but still shares in-flight work.
  • client.clearToken() also invalidates this client’s chart query cache. If an external token provider changes identity without calling clearToken(), create a new authentication-scoped client or call clearUsageChartCache(client) and remount/refetch active charts. Do not share a client/cache across auth scopes.
Prop Default Description
client Provider client Optional explicit shared client
type line Built-in or registered chart type
mode Inferred trend or category; pie/doughnut and registered polar types always use category mode
query — Partial API query, with snake_case fields and no limit
rangeDays 7 Relative time range; explicit query timestamps take precedence
measure First query measure or quantity One additive quantity or amount measure
limit 500 Aggregate API result limit, subject to backend constraints
cacheTimeMs 30000 Completed-result cache TTL
allowPartial false Explicitly render potentially truncated data
refreshable true Built-in Refresh action
formatValue Default number formatter Optional value formatter; does not affect fetching or aggregation
title, caption, height, showLegend, showGrid, theme Existing chart defaults Presentation only; category charts show legends by default
loading, error, slots, messages Existing defaults Custom loading/error rendering and header/chart slots
className, classNames, unstyled Existing defaults Existing semantic styles

Custom error renderers replace the built-in Retry UI. The new localized notice key is usageChartTruncated; existing refresh, retry, usageLoading and chart messages are reused. No data, x or series props are required.

import { createUsageChartController } from '@meterry/component'
const controller = createUsageChartController({ client, query: { metrics: ['total_tokens'] } })
const unsubscribe = controller.subscribe((state) => console.log(state))
await controller.load()
await controller.refresh()
unsubscribe()
controller.dispose()

UsageChartState contains status, result and error. UsageChartResult contains mode, measure, data, series, sourceCount and truncated; its rows have label, optional raw bucket, and values. For direct model rendering use createChartModel({ type, data: result.data, x: 'label', series: result.series }). Headless callers decide how to present or suppress a possibly truncated result.

Pure helpers createUsageChartRequest, aggregateUsageTrend, aggregateUsageCategories, resolveUsageChartMode and serializeUsageChartQuery are also exported. MeterChart accepts optional request: ChartRequestState plus loading/error rendering for consumers who already own request state but want the same lifecycle UI.

The Analysis API has a maximum of four grouping dimensions (including the automatically added bucket and metric), a 31-local-calendar-date range constraint, minimum bucket sizes and a 1000-row maximum limit. rangeDays does not bypass validation, and limit is not an event batch size. Use Usage Query for the category contract; it has no timezone or cursor. The component default metric is tokens; examples select metrics explicitly, and those names must exist in your project and be allowed by the Subject Token.

For measure="amount", filter to a comparable currency and unit before aggregation. Display formatting does not convert currencies, and JavaScript-number chart aggregation is not an accounting or invoice-calculation API. The existing MeterUsage total/summary semantics are unchanged; this new component does not silently replace it.