Skip to content

MeterChart

A chart component that renders the rows you pass as data: data + series go in, core’s createChartModel builds a headless model, and ECharts renders it. The rows themselves come from the meter service — query analysis/query (or usage/query) through the client, map the response into rows, and hand them to the chart.

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 { useEffect, useState } from "react"
import { MeterChart } from "@meterry/component-react"
import type {
MeterThemeInput,
MeterFilterValues,
} from "@meterry/component"
import {
fetchPreviewTrend,
type PreviewEnvironment,
type TrendRow,
} from "./preview-data"
import { EMPTY_FILTERS, styleProps, trendSeries } from "./preview-shared"
export function ChartPreview({
environment,
theme,
filters = EMPTY_FILTERS,
}: {
environment: PreviewEnvironment
theme: MeterThemeInput
filters?: MeterFilterValues
}) {
const [type, setType] = useState("line")
const [visibleSeries, setVisibleSeries] = useState("both")
const [showLegend, setShowLegend] = useState(true)
const [showGrid, setShowGrid] = useState(true)
const [state, setState] = useState<{
rows: TrendRow[]
loading: boolean
error?: Error
}>({ rows: [], loading: true })
const filtersKey = JSON.stringify(filters)
useEffect(() => {
const abort = new AbortController()
setState({ rows: [], loading: true })
void fetchPreviewTrend(environment, JSON.parse(filtersKey), abort.signal)
.then((rows) => {
if (!abort.signal.aborted) setState({ rows, loading: false })
})
.catch((error: unknown) => {
if (!abort.signal.aborted)
setState({
rows: [],
loading: false,
error: error instanceof Error ? error : new Error(String(error)),
})
})
return () => abort.abort()
}, [environment, filtersKey])
return (
<div data-testid="chart-preview">
<label className="docs-preview-chart-select">
Chart type
<select value={type} onChange={(event) => setType(event.target.value)}>
<option value="line">Line</option>
<option value="bar">Bar</option>
<option value="area">Area</option>
</select>
</label>
<div className="docs-preview-actions docs-preview-chart-controls">
<label className="docs-preview-chart-select">
Chart series
<select
value={visibleSeries}
onChange={(event) => setVisibleSeries(event.target.value)}
>
<option value="both">Input and output</option>
<option value="input">Input only</option>
<option value="output">Output only</option>
</select>
</label>
<label className="docs-preview-chart-select">
<input
type="checkbox"
checked={showLegend}
onChange={(event) => setShowLegend(event.target.checked)}
/>
Show legend
</label>
<label className="docs-preview-chart-select">
<input
type="checkbox"
checked={showGrid}
onChange={(event) => setShowGrid(event.target.checked)}
/>
Show grid
</label>
</div>
{state.loading ? (
<p className="docs-preview-status" role="status">
Loading chart…
</p>
) : state.error ? (
<p className="docs-preview-status" role="alert">
{state.error.message}
</p>
) : (
<MeterChart
{...styleProps}
theme={theme}
type={type}
title="Token usage · last 7 days"
data={state.rows}
x="bucket"
height={260}
showLegend={showLegend}
showGrid={showGrid}
series={trendSeries.filter(
(series) => visibleSeries === "both" || series.key === visibleSeries
)}
/>
)}
</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.

The examples below query the Subject API through a shared client, map the response into rows, and render those rows. See Getting Started for the complete query-to-chart wiring.

Fetch rows from the meter service, then render:

// React: fetch with the client, map, render
function DailyTokensChart() {
const client = useMeterClient()
const [rows, setRows] = useState([])
useEffect(() => {
if (!client) return
const end = Math.floor(Date.now() / 1000)
client
.analysisQuery({
bucket_size: '1d',
start_time: end - 14 * 86_400,
end_time: end,
metrics: ['input_tokens', 'output_tokens'],
group_by: ['bucket'],
measures: ['quantity'],
})
.then((response) => setRows(pivotByBucket(response))) // series → rows
.catch((error) => console.error(error))
}, [client])
return (
<MeterChart
type="line"
title="Token usage · last 14 days"
data={rows}
x="bucket"
series={[
{ key: 'input', label: 'Input tokens', value: 'input_tokens' },
{ key: 'output', label: 'Output tokens', value: 'output_tokens' },
]}
/>
)
}
<!-- Vue 3 -->
<script setup>
import { onMounted, ref } from 'vue'
import { useMeterClient } from '@meterry/component-vue'
const client = useMeterClient()
const rows = ref([])
onMounted(async () => {
const end = Math.floor(Date.now() / 1000)
const response = await client.analysisQuery({
bucket_size: '1d',
start_time: end - 14 * 86_400,
end_time: end,
metrics: ['input_tokens', 'output_tokens'],
group_by: ['bucket'],
measures: ['quantity'],
})
rows.value = pivotByBucket(response) // series → rows
})
</script>
<template>
<MeterChart
type="line"
title="Token usage · last 14 days"
:data="rows"
x="bucket"
:series="[
{ key: 'input', label: 'Input tokens', value: 'input_tokens' },
{ key: 'output', label: 'Output tokens', value: 'output_tokens' },
]"
/>
</template>
Prop Type Default Description
type ChartType — (required) 'line' | 'bar' | 'area' | 'pie' | 'doughnut' or a registered custom type
data T[] — (required) Row data — typically the mapped result of a client query
series ChartSeries<T>[] — (required) { key, label?, value, color?, format? }; value accepts a field name or (row, index) => unknown
x ChartAccessor<T> row index x-axis field or accessor function
title string messages.chartTitle Title (also used as the chart’s aria-label)
height number 300 Chart height in px
showLegend boolean series.length > 1 Whether to show the legend
showGrid boolean true Whether to show the grid
theme MeterThemeInput 'light' Chart palette (pair with CSS variables to reskin the whole card, see Theming & Styles)
caption string messages.caption Small label above the title
slots MeterChartSlots — Replace the header / chart rendering
messages Partial<MeterMessages> — Copy overrides
seriesRegistry EChartsSeriesRegistry global registry Scoped chart-type registry
className / classNames / unstyled — — Styling

Choose the chart type with type and the measures to display with series. Use a descriptive key for each measure, such as input_tokens or amount. To show a different dataset, pass the new rows through data.

showGrid={false} (Vue: :show-grid="false") hides Cartesian split lines; showLegend={false} hides the legend. Try Chart series, Show grid, and Show legend in the preview to choose which measures and visual guides are shown.

Use series.format to make tooltip values easier to read, for example by showing an amount with its currency. Formatting changes the display, not the usage values:

<MeterChart
type="bar"
data={rows}
x="bucket"
series={[{
key: 'amount',
label: 'Amount (USD)',
value: 'amount',
format: (value, row) => `${Number(value).toFixed(4)} USD · ${row.bucket}`,
}]}
/>

To add a chart type beyond the built-in options, see Chart Extension & Registries.

When data is empty or series has no drawable points, the messages.chartEmpty empty state is rendered. Query a window with no usage (for example a future range) to exercise it against the live service.

type is an open union: register an ECharts mapping with registerEChartsSeries(type, builder, { modules?, polar? }), or register a pure SVG renderer with registerChartSvgRenderer (for ECharts-free setups). See Chart Extension & Registries.

When you want the model without the component:

import { createChartModel, createEChartsOption, renderChartSvg } from '@meterry/component'
// rows: mapped from a client query (see Getting Started)
const model = createChartModel({ type: 'line', data: rows, x: 'bucket', series })
const option = createEChartsOption(model) // feeds echarts directly
const svg = renderChartSvg(model) // pure SVG string (register a renderer or use a built-in type)