ECharts

ECharts is one of the most powerful charting libraries available -- heatmaps, treemaps, graph visualizations, geographic maps, 3D charts. It renders on canvas, which means CSS has no effect at all. Theming requires a third strategy beyond the SCSS bridge and runtime style injection: injecting XMLUI design tokens directly into the ECharts option object at runtime.

Option-level theming

The render component resolves the XMLUI color palette (color-primary-500, color-success-500, etc.) to actual color values via useTheme(), then merges them into the option as defaults. User-provided colors in the option override the defaults. Axis labels, tooltip backgrounds, legend text -- everything inherits from XMLUI tokens unless explicitly overridden.

This is the useTheme() pattern from the Calendar page, taken further. The calendar injects scoped CSS; ECharts injects a JavaScript object. Same hook, different target.

Native event capture

Native event capture is comprehensive: clicks on data points, legend toggles, zoom/pan, brush selections, timeline changes. The render component maps each to a structured onNativeEvent call with a meaningful displayLabel -- for example, "Commits → Wed = 150" for a bar click, or "TypeScript: hide" for a legend toggle.

Multi-series time series with zoom

Drag the slider handles below the chart to zoom into a date range.

<App>
  <EChart
    aria-label="Demo time series chart"
    height="450px"
    option="{
      (() => {
        const days = [];
        const commits = [];
        const reviews = [];
        const deploys = [];
        const incidents = [];
        const d = new Date('2025-09-01');
        for (let i = 0; i < 180; i++) {
          days.push(new Date(d).toISOString().slice(0, 10));
          const dow = d.getDay();
          const weekend = dow === 0 || dow === 6;
          commits.push(weekend ? Math.floor(Math.random() * 5) : Math.floor(Math.random() * 40 + 10));
          reviews.push(weekend ? Math.floor(Math.random() * 3) : Math.floor(Math.random() * 25 + 5));
          deploys.push(weekend ? 0 : Math.floor(Math.random() * 4));
          incidents.push(Math.random() < 0.08 ? Math.floor(Math.random() * 3 + 1) : 0);
          d.setDate(d.getDate() + 1);
        }
        return {
          grid: { bottom: 100 },
          xAxis: { type: 'time' },
          yAxis: { type: 'value' },
          series: [
            { type: 'line', name: 'Commits', smooth: true, showSymbol: false, data: days.map((d, i) => [d, commits[i]]) },
            { type: 'line', name: 'Reviews', smooth: true, showSymbol: false, data: days.map((d, i) => [d, reviews[i]]) },
            { type: 'line', name: 'Deploys', smooth: true, showSymbol: false, data: days.map((d, i) => [d, deploys[i]]) },
            { type: 'line', name: 'Incidents', smooth: true, showSymbol: false, data: days.map((d, i) => [d, incidents[i]]) }
          ],
          tooltip: { trigger: 'axis' },
          legend: { bottom: 50 },
          dataZoom: [{ type: 'slider', bottom: 10 }]
        };
      })()
    }" />
</App>
<App>
  <EChart
    aria-label="Demo time series chart"
    height="450px"
    option="{
      (() => {
        const days = [];
        const commits = [];
        const reviews = [];
        const deploys = [];
        const incidents = [];
        const d = new Date('2025-09-01');
        for (let i = 0; i < 180; i++) {
          days.push(new Date(d).toISOString().slice(0, 10));
          const dow = d.getDay();
          const weekend = dow === 0 || dow === 6;
          commits.push(weekend ? Math.floor(Math.random() * 5) : Math.floor(Math.random() * 40 + 10));
          reviews.push(weekend ? Math.floor(Math.random() * 3) : Math.floor(Math.random() * 25 + 5));
          deploys.push(weekend ? 0 : Math.floor(Math.random() * 4));
          incidents.push(Math.random() < 0.08 ? Math.floor(Math.random() * 3 + 1) : 0);
          d.setDate(d.getDate() + 1);
        }
        return {
          grid: { bottom: 100 },
          xAxis: { type: 'time' },
          yAxis: { type: 'value' },
          series: [
            { type: 'line', name: 'Commits', smooth: true, showSymbol: false, data: days.map((d, i) => [d, commits[i]]) },
            { type: 'line', name: 'Reviews', smooth: true, showSymbol: false, data: days.map((d, i) => [d, reviews[i]]) },
            { type: 'line', name: 'Deploys', smooth: true, showSymbol: false, data: days.map((d, i) => [d, deploys[i]]) },
            { type: 'line', name: 'Incidents', smooth: true, showSymbol: false, data: days.map((d, i) => [d, incidents[i]]) }
          ],
          tooltip: { trigger: 'axis' },
          legend: { bottom: 50 },
          dataZoom: [{ type: 'slider', bottom: 10 }]
        };
      })()
    }" />
</App>

Line chart with toolbox

Click legend items to toggle series on and off.

<App>
  <EChart
    aria-label="Demo line chart with toolbox"
    height="400px"
    option="{
      {
        xAxis: { type: 'category', data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] },
        yAxis: { type: 'value' },
        series: [
          { type: 'line', name: 'Users', data: [820, 932, 901, 934, 1290, 1330, 1520, 1480, 1610, 1750, 1830, 1920], smooth: true },
          { type: 'line', name: 'Sessions', data: [1200, 1400, 1350, 1500, 1800, 2100, 2300, 2150, 2400, 2600, 2750, 2900], smooth: true }
        ],
        tooltip: { trigger: 'axis' },
        legend: {},
        toolbox: {
          feature: {
            magicType: { type: ['line', 'bar', 'stack'] },
            dataZoom: {},
            restore: {},
            saveAsImage: {}
          }
        }
      }
    }" />
</App>
<App>
  <EChart
    aria-label="Demo line chart with toolbox"
    height="400px"
    option="{
      {
        xAxis: { type: 'category', data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] },
        yAxis: { type: 'value' },
        series: [
          { type: 'line', name: 'Users', data: [820, 932, 901, 934, 1290, 1330, 1520, 1480, 1610, 1750, 1830, 1920], smooth: true },
          { type: 'line', name: 'Sessions', data: [1200, 1400, 1350, 1500, 1800, 2100, 2300, 2150, 2400, 2600, 2750, 2900], smooth: true }
        ],
        tooltip: { trigger: 'axis' },
        legend: {},
        toolbox: {
          feature: {
            magicType: { type: ['line', 'bar', 'stack'] },
            dataZoom: {},
            restore: {},
            saveAsImage: {}
          }
        }
      }
    }" />
</App>

Donut chart

<App>
  <EChart
    aria-label="Demo donut chart"
    height="400px"
    option="{
      {
        series: [{
          type: 'pie',
          radius: ['40%', '70%'],
          data: [
            { name: 'TypeScript', value: 45 },
            { name: 'Python', value: 25 },
            { name: 'Go', value: 15 },
            { name: 'Rust', value: 10 },
            { name: 'Other', value: 5 }
          ],
          label: { show: true, formatter: '{b}: {d}%' }
        }],
        tooltip: { trigger: 'item' },
        legend: { bottom: 0, left: 'center' }
      }
    }" />
</App>
<App>
  <EChart
    aria-label="Demo donut chart"
    height="400px"
    option="{
      {
        series: [{
          type: 'pie',
          radius: ['40%', '70%'],
          data: [
            { name: 'TypeScript', value: 45 },
            { name: 'Python', value: 25 },
            { name: 'Go', value: 15 },
            { name: 'Rust', value: 10 },
            { name: 'Other', value: 5 }
          ],
          label: { show: true, formatter: '{b}: {d}%' }
        }],
        tooltip: { trigger: 'item' },
        legend: { bottom: 0, left: 'center' }
      }
    }" />
</App>

Treemap -- filesystem storage

<App>
  <EChart
    aria-label="Filesystem storage treemap"
    height="500px"
    option="{
      {
        series: [{
          type: 'treemap',
          roam: false,
          leafDepth: 2,
          levels: [
            { itemStyle: { borderWidth: 2, borderColor: '#666', gapWidth: 2 } },
            { itemStyle: { borderWidth: 1, borderColor: '#aaa', gapWidth: 1 }, upperLabel: { show: true, height: 20 } },
            { itemStyle: { borderWidth: 1, borderColor: '#ccc' } }
          ],
          data: [
            { name: 'src', value: 4200, children: [
              { name: 'components', value: 1800, children: [
                { name: 'Button.tsx', value: 120 },
                { name: 'Dialog.tsx', value: 280 },
                { name: 'Table.tsx', value: 450 },
                { name: 'Form.tsx', value: 380 },
                { name: 'Layout.tsx', value: 320 },
                { name: 'Chart.tsx', value: 250 }
              ]},
              { name: 'utils', value: 600, children: [
                { name: 'parser.ts', value: 220 },
                { name: 'format.ts', value: 180 },
                { name: 'validate.ts', value: 200 }
              ]},
              { name: 'core', value: 1200, children: [
                { name: 'engine.ts', value: 480 },
                { name: 'renderer.ts', value: 350 },
                { name: 'state.ts', value: 370 }
              ]},
              { name: 'styles', value: 600, children: [
                { name: 'theme.scss', value: 180 },
                { name: 'tokens.scss', value: 150 },
                { name: 'components.scss', value: 270 }
              ]}
            ]},
            { name: 'node_modules', value: 12000, children: [
              { name: 'react', value: 2800 },
              { name: 'echarts', value: 3200 },
              { name: 'typescript', value: 2100 },
              { name: '@tiptap', value: 1800 },
              { name: 'radix-ui', value: 1200 },
              { name: 'other', value: 900 }
            ]},
            { name: 'dist', value: 3500, children: [
              { name: 'bundle.js', value: 2200 },
              { name: 'vendor.js', value: 800 },
              { name: 'styles.css', value: 300 },
              { name: 'assets', value: 200 }
            ]},
            { name: 'tests', value: 1500, children: [
              { name: 'unit', value: 600 },
              { name: 'integration', value: 500 },
              { name: 'e2e', value: 400 }
            ]}
          ]
        }],
        tooltip: { formatter: function(p) { return p.name + ': ' + (p.value / 1000).toFixed(1) + ' MB'; } }
      }
    }" />
</App>
<App>
  <EChart
    aria-label="Filesystem storage treemap"
    height="500px"
    option="{
      {
        series: [{
          type: 'treemap',
          roam: false,
          leafDepth: 2,
          levels: [
            { itemStyle: { borderWidth: 2, borderColor: '#666', gapWidth: 2 } },
            { itemStyle: { borderWidth: 1, borderColor: '#aaa', gapWidth: 1 }, upperLabel: { show: true, height: 20 } },
            { itemStyle: { borderWidth: 1, borderColor: '#ccc' } }
          ],
          data: [
            { name: 'src', value: 4200, children: [
              { name: 'components', value: 1800, children: [
                { name: 'Button.tsx', value: 120 },
                { name: 'Dialog.tsx', value: 280 },
                { name: 'Table.tsx', value: 450 },
                { name: 'Form.tsx', value: 380 },
                { name: 'Layout.tsx', value: 320 },
                { name: 'Chart.tsx', value: 250 }
              ]},
              { name: 'utils', value: 600, children: [
                { name: 'parser.ts', value: 220 },
                { name: 'format.ts', value: 180 },
                { name: 'validate.ts', value: 200 }
              ]},
              { name: 'core', value: 1200, children: [
                { name: 'engine.ts', value: 480 },
                { name: 'renderer.ts', value: 350 },
                { name: 'state.ts', value: 370 }
              ]},
              { name: 'styles', value: 600, children: [
                { name: 'theme.scss', value: 180 },
                { name: 'tokens.scss', value: 150 },
                { name: 'components.scss', value: 270 }
              ]}
            ]},
            { name: 'node_modules', value: 12000, children: [
              { name: 'react', value: 2800 },
              { name: 'echarts', value: 3200 },
              { name: 'typescript', value: 2100 },
              { name: '@tiptap', value: 1800 },
              { name: 'radix-ui', value: 1200 },
              { name: 'other', value: 900 }
            ]},
            { name: 'dist', value: 3500, children: [
              { name: 'bundle.js', value: 2200 },
              { name: 'vendor.js', value: 800 },
              { name: 'styles.css', value: 300 },
              { name: 'assets', value: 200 }
            ]},
            { name: 'tests', value: 1500, children: [
              { name: 'unit', value: 600 },
              { name: 'integration', value: 500 },
              { name: 'e2e', value: 400 }
            ]}
          ]
        }],
        tooltip: { formatter: function(p) { return p.name + ': ' + (p.value / 1000).toFixed(1) + ' MB'; } }
      }
    }" />
</App>

Click data points, toggle legend items, use the toolbox, drag the zoom slider to see native event traces. The charts rebuild with new colors on every theme or tone change.