pyreon

@pyreon/charts provides a reactive bridge to Apache ECharts for Pyreon applications. Chart modules are lazy-loaded on demand -- zero bundle cost until a chart actually renders. The Canvas renderer is used by default, with SVG available as an option.

@pyreon/chartsstable

Installation

npm install @pyreon/charts
bun add @pyreon/charts
pnpm add @pyreon/charts
yarn add @pyreon/charts

Quick Start

Use the <Chart /> component to render a chart. Pass an options function that returns a standard ECharts configuration -- signal reads inside the function are tracked for reactivity.

import { signal } from '@pyreon/reactivity'
import { Chart } from '@pyreon/charts'

function SalesChart() {
  const months = signal(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'])
  const revenue = signal([120, 200, 150, 80, 70, 110])

  return (
    <Chart
      options={() => ({
        xAxis: { type: 'category', data: months() },
        yAxis: { type: 'value' },
        tooltip: { trigger: 'axis' },
        series: [{ name: 'Revenue', type: 'bar', data: revenue() }],
      })}
      style="height: 400px"
    />
  )
}

The options prop accepts a function (not a plain object) so that signal reads are tracked. When any signal inside the function changes, the chart re-renders automatically.

API Reference

<Chart />

The primary component for rendering charts.

PropTypeDescription
options() => EChartsOptionFunction returning ECharts configuration. Signal reads are tracked for reactivity.
stylestringInline style string. Must include a height (ECharts requires a sized container).
classstringCSS class name for the container element.
renderer'canvas' | 'svg'Rendering mode. Defaults to 'canvas'.
onChartReady(instance: ECharts) => voidCallback fired after the chart instance is initialized.
on*Event handlersECharts event bindings, e.g. onClick, onMouseover, onLegendSelectChanged.
<Chart
  options={() => ({
    /* ... */
  })}
  style="height: 300px"
  renderer="svg"
  onClick={(params) => console.log('Clicked:', params.name)}
  onChartReady={(instance) => console.log('Chart ready:', instance)}
/>

useChart<TOption>(optionsFn, config?)

A lower-level hook for programmatic control. Returns reactive signals for the chart instance and error state.

import { useChart } from '@pyreon/charts'

function MyChart() {
  const { containerRef, instance, error } = useChart(() => ({
    xAxis: { type: 'category', data: ['A', 'B', 'C'] },
    yAxis: { type: 'value' },
    series: [{ type: 'bar', data: [10, 20, 30] }],
  }))

  return (
    <div>
      {() => (error() ? <p class="error">{error()!.message}</p> : null)}
      <div ref={(el) => containerRef.set(el)} style="height: 400px" />
    </div>
  )
}

Config options:

OptionTypeDefaultDescription
renderer'canvas' | 'svg''canvas'Rendering mode
notMergebooleanfalseReplace options entirely instead of merging
lazyUpdatebooleanfalseDefer chart update to next frame

Return value:

PropertyTypeDescription
containerRefSignal<HTMLElement | null>Bind to a DOM element via ref
instanceSignal<ECharts | null>The underlying ECharts instance (available after init)
errorSignal<Error | null>Error signal for init or setOption failures

Types

@pyreon/charts re-exports all ECharts option types for strict typing:

import type {
  ComposeOption,
  BarSeriesOption,
  LineSeriesOption,
  PieSeriesOption,
  ScatterSeriesOption,
  RadarSeriesOption,
  HeatmapSeriesOption,
  TreemapSeriesOption,
  SankeySeriesOption,
  GaugeSeriesOption,
  FunnelSeriesOption,
  CandlestickSeriesOption,
  GraphSeriesOption,
} from '@pyreon/charts'

Auto-Detection

When you provide an options function, @pyreon/charts inspects the configuration to determine which ECharts modules are needed:

  • Series types (bar, line, pie, etc.) are detected from series[].type

  • Components (tooltip, legend, dataZoom, etc.) are detected from top-level keys

  • Axis types (category, value, time, log) are detected from axis config

Only the required modules are dynamically imported. A chart with type: 'bar' and tooltip will only load the bar series renderer and tooltip component -- not the full ECharts bundle.

// Only loads: BarChart, TooltipComponent, GridComponent, CanvasRenderer
<Chart
  options={() => ({
    tooltip: { trigger: 'axis' },
    xAxis: { type: 'category', data: labels() },
    yAxis: { type: 'value' },
    series: [{ type: 'bar', data: values() }],
  })}
  style="height: 300px"
/>

Strict Typing with ComposeOption

For type-safe chart configurations, use ComposeOption<> to narrow the options type to only the series types you use:

import { useChart } from '@pyreon/charts'
import type { ComposeOption, BarSeriesOption, LineSeriesOption } from '@pyreon/charts'

type DashboardOption = ComposeOption<BarSeriesOption | LineSeriesOption>

function Dashboard() {
  const chart = useChart<DashboardOption>(() => ({
    xAxis: { type: 'category', data: ['Q1', 'Q2', 'Q3', 'Q4'] },
    yAxis: { type: 'value' },
    series: [
      { type: 'bar', data: [100, 200, 150, 300] },
      { type: 'line', data: [80, 170, 130, 280] },
    ],
  }))

  return <div ref={(el) => chart.containerRef.set(el)} style="height: 400px" />
}

This gives you autocomplete and type checking for the specific series options you declared.

Manual Registration

For maximum tree-shaking control, use the @pyreon/charts/manual entry point. This disables auto-detection and requires you to register ECharts modules explicitly:

import { useChart, registerModules } from '@pyreon/charts/manual'
import { BarChart, LineChart } from 'echarts/charts'
import { TooltipComponent, GridComponent, LegendComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'

// Register once at app startup
registerModules([
  BarChart,
  LineChart,
  TooltipComponent,
  GridComponent,
  LegendComponent,
  CanvasRenderer,
])

// Then use useChart / <Chart /> as normal
function MyChart() {
  const chart = useChart(() => ({
    series: [{ type: 'bar', data: [1, 2, 3] }],
  }))

  return <div ref={(el) => chart.containerRef.set(el)} style="height: 300px" />
}

Use manual registration when you need deterministic bundle sizes or are building a library that should not auto-import ECharts modules.

Bundle Size

ImportApproximate Size (gzipped)
@pyreon/charts (wrapper only)~2 KB
+ Bar chart~15 KB
+ Line chart~18 KB
+ Pie chart~12 KB
+ Tooltip + Legend~8 KB
Full ECharts (all modules)~300 KB

Auto-detection ensures you only pay for what you use. A typical dashboard with 2-3 chart types loads ~40-50 KB of ECharts code.

Error Handling

Both <Chart /> and useChart() expose an error signal that captures initialization and rendering failures:

import { Chart } from '@pyreon/charts'

function SafeChart() {
  return (
    <Chart
      options={() => ({
        series: [{ type: 'bar', data: chartData() }],
      })}
      style="height: 300px"
      onError={(err) => console.error('Chart error:', err)}
    />
  )
}

With useChart(), check the error signal directly:

import { useChart } from '@pyreon/charts'

function SafeChart() {
  const { containerRef, error } = useChart(() => ({
    series: [{ type: 'bar', data: chartData() }],
  }))

  return (
    <div>
      {() =>
        error() ? (
          <div class="chart-error">
            <p>Failed to render chart: {error()!.message}</p>
          </div>
        ) : null
      }
      <div ref={(el) => containerRef.set(el)} style="height: 300px" />
    </div>
  )
}

Common error scenarios:

  • Container element has zero height (ECharts requires a sized container)

  • Invalid option structure passed to setOption

  • Network failure when lazy-loading ECharts modules

Charts