@cloudflare/kumo

Accessibility and localization

The low-level Chart component accepts Kumo-owned ariaLabel and ariaDescription props. Provide localized text that names the chart and summarizes the data or trend for screen reader users. Register ECharts’ AriaComponent when you want ECharts to generate its own data summary from the chart options.

<Chart
  echarts={echarts}
  options={options}
  ariaLabel={t("charts.planBreakdown.label")}
  ariaDescription={t("charts.planBreakdown.description")}
/>

Custom Chart

import { Chart } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { EChartsOption } from "echarts";
import { useMemo } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";

export function PieChartDemo() {
  const isDarkMode = useIsDarkMode();

  const options = useMemo<EChartsOption>(
    () => ({
      animation: true,
      animationDuration: 2000,
      tooltip: {
        show: true,
      },
      series: [
        {
          type: "pie",
          data: [
            { value: 101, name: "Series A" },
            { value: 202, name: "Series B" },
            { value: 303, name: "Series C" },
            { value: 404, name: "Series D" },
            { value: 505, name: "Series E" },
          ],
        },
      ],
    }),
    [],
  );

  return (
    <Chart
      echarts={echarts}
      options={options}
      height={400}
      isDarkMode={isDarkMode}
    />
  );
}

Custom Tooltip with HTML

For tooltips that require custom HTML formatting, use the dangerousHtmlFormatter property instead of the standard formatter. This makes the security implications more explicit.

When using dangerousHtmlFormatter, it is strongly recommended to sanitize any user-provided content using echarts.format.encodeHTML to prevent XSS vulnerabilities.

import { Chart } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { EChartsOption } from "echarts";
import { useMemo } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";

/**
 * Custom chart with HTML tooltip using dangerousHtmlFormatter.
 * USE WITH CAUTION: Only use dangerousHtmlFormatter for trusted HTML content.
 * Always sanitize any user-provided data using echarts.format.encodeHTML
 * or similar utilities to prevent XSS vulnerabilities.
 */
export function CustomTooltipChartDemo() {
  const isDarkMode = useIsDarkMode();

  const options = useMemo<EChartsOption>(
    () => ({
      tooltip: {
        trigger: "item",
        formatter: (params: any) => {
          // IMPORTANT: Always escape ALL dynamic values using encodeHTML
          // from echarts/format before including in HTML. This prevents
          // XSS attacks from malicious data like:
          // { name: "<img src=x onerror=alert('xss')>", value: "..." }
          const safeName = echarts.format.encodeHTML(params.name);
          const safeValue = echarts.format.encodeHTML(String(params.value));
          const safePercent = echarts.format.encodeHTML(
            String(Math.round(params.percent)),
          );

          return `
            <div style="padding: 8px;">
              <div style="font-weight: 600; margin-bottom: 4px;">${safeName}</div>
              <div>Value: <strong>${safeValue}</strong></div>
              <div style="font-size: 12px; opacity: 0.7; margin-top: 4px;">
                ${safePercent}% of total
              </div>
            </div>
          `;
        },
      },
      series: [
        {
          type: "pie",
          data: [
            { value: 101, name: "Series A" },
            { value: 202, name: "Series B" },
            // Malicious series name to demonstrate XSS protection via encodeHTML.
            // Without encoding, this would render an alert popup. With encodeHTML,
            // it safely displays as plain text.
            { value: 150, name: "<img src=x onerror=alert('XSS')>" },
            { value: 303, name: "Series C" },
            { value: 404, name: "Series D" },
          ],
        },
      ],
    }),
    [],
  );

  return (
    <Chart
      echarts={echarts}
      options={options}
      height={400}
      isDarkMode={isDarkMode}
    />
  );
}