# Charts (/docs/charts) Generate a chart as an SVG string, then pass it to an `` in your document. Takumi renders the SVG without running the chart library in a browser. The library must support generating SVG in your runtime. ## Render an ECharts chart [#render-an-echarts-chart] Install ECharts alongside `takumi-js`: npm pnpm yarn bun ```bash npm i echarts takumi-js @takumi-rs/helpers react ``` ```bash pnpm add echarts takumi-js @takumi-rs/helpers react ``` ```bash yarn add echarts takumi-js @takumi-rs/helpers react ``` ```bash bun add echarts takumi-js @takumi-rs/helpers react ``` This example generates a bar chart and saves it as a PNG. ECharts uses its SVG server renderer, so it needs no DOM or canvas. ```tsx twoslash import * as echarts from "echarts"; import { render } from "takumi-js"; import { googleFonts } from "@takumi-rs/helpers"; import { writeFile } from "node:fs/promises"; const chart = echarts.init(null, null, { renderer: "svg", ssr: true, width: 560, height: 360, }); chart.setOption({ animation: false, textStyle: { fontFamily: "Inter" }, xAxis: { type: "category", data: ["Q1", "Q2", "Q3", "Q4"] }, yAxis: { type: "value" }, series: [{ type: "bar", data: [320, 730, 550, 910] }], }); const svg = chart.renderToSVGString(); chart.dispose(); const fonts = (await googleFonts(["Inter"])).map((font) => ({ ...font, ranges: [] })); const png = await render( Quarterly revenue: 320, 730, 550, and 910, { width: 560, height: 360, fonts }, ); await writeFile("revenue.png", png); ``` Keep `animation: false` for static output. Animated SVG can capture an empty or partially drawn chart. Dispose of the ECharts instance after extracting the SVG. The SVG string goes directly in `src`. It does not need an `images` entry. To include it in an HTML string, insert the SVG markup inside the document. For PDF output, see [Charts in PDF](/docs/pdf/charts). ## Fonts for chart labels [#fonts-for-chart-labels] Register fonts that cover every label, tick, and legend entry. Set the chart's font family to the same name. `googleFonts()` returns font subsets with Unicode ranges. Takumi filters those subsets against text in the document tree, which excludes text inside an SVG image. Clearing `ranges`, as above, keeps every returned subset. For a large font family, select the subsets against the chart text first. Include generated numeric ticks and punctuation as well as your data labels: ```tsx twoslash import { googleFonts, subsetFonts } from "@takumi-rs/helpers"; const chartText = "一月二月三月四月營收0123456789.,−-%"; const fonts = subsetFonts({ fonts: await googleFonts(["Noto Sans TC"]), source: chartText, }).map((font) => ({ ...font, ranges: [] })); ``` Pass `fonts` to `render()` and set ECharts `textStyle.fontFamily` to `"Noto Sans TC"`. For offline output, load bundled font bytes instead. See [Fonts in CI and offline renders](/docs/typography-and-fonts#fonts-in-ci-and-offline-renders). ## Use another chart library [#use-another-chart-library] Takumi accepts the SVG output, not the library's interactive component. Generate the final chart before calling `render()`. | Approach | SVG generation | | --------------------- | --------------------------------------------------------- | | ECharts | `ssr: true`, then `renderToSVGString()` | | Vega or Vega-Lite | Create a Vega view and call `toSVG()` | | d3-shape and d3-scale | Build SVG elements from computed paths and coordinates | | React SVG components | Render supported components with `renderToStaticMarkup()` | A library that requires a DOM or canvas needs its own server rendering setup. If it produces a PNG, pass that image to Takumi instead. A raster chart remains raster in PDF. # Comparison to satori (/docs/comparison-to-satori) [satori](https://github.com/vercel/satori) pioneered OG images without a headless browser and powers `next/og`. It turns JSX into an SVG string, so a bitmap takes a pipeline: yoga computes layout in WebAssembly, satori emits SVG, and `resvg` or `sharp` rasterizes it. Takumi is one Rust engine that does the whole job: JSX in, encoded image out. Migration is mostly an import swap: `ImageResponse` matches the `next/og` API, and templates written for satori declare `display: flex` explicitly, so they render unchanged. Compare rendered output across providers at [image-bench.kane.tw](https://image-bench.kane.tw). ## Features [#features] ### Templates and styling [#templates-and-styling] | Feature | satori / `next/og` | Takumi | | :---------------------------------- | :----------------------- | :---------------------------- | | Template input | JSX | JSX, HTML strings, node trees | | Styling | Inline styles, `tw` prop | + ` ``` ### Render the component on a server route [#render-the-component-on-a-server-route] * Import the component, and `app.css` if you use Tailwind. * Render to HTML with `render` from `svelte/server`. * Return an `ImageResponse`. ```ts title="src/routes/+server.ts" import { render } from "svelte/server"; import style from "../app.css?inline"; import ImageResponse from "takumi-js/response"; import OgImage from "$lib/components/OgImage.svelte"; import type { RequestEvent } from "./$types"; export async function GET({ url }: RequestEvent) { const { body, head } = await render(OgImage, { props: { name: url.searchParams.get("name") ?? "Goo goo gaga", }, }); return new ImageResponse(`${head}${body}`, { width: 1200, height: 630, // [!code highlight] css: style, fonts: ["https://takumi.kane.tw/fonts/Geist.woff2"], }); } ``` # TanStack Start (/docs/integration/tanstack-start) ### Install Takumi [#install-takumi] npm pnpm yarn bun ```bash npm i takumi-js ``` ```bash pnpm add takumi-js ``` ```bash yarn add takumi-js ``` ```bash bun add takumi-js ``` ### Create a server file route [#create-a-server-file-route] TanStack Start lets you define HTTP handlers on a file route with `server.handlers`. ```tsx title="src/routes/og-image.tsx" import { createFileRoute } from "@tanstack/react-router"; import ImageResponse from "takumi-js/response"; export const Route = createFileRoute("/og-image")({ server: { handlers: { GET({ request }) { const url = new URL(request.url); const title = url.searchParams.get("title") ?? "Takumi + TanStack Start"; const description = url.searchParams.get("description") ?? "Render OG images from a route handler."; return new ImageResponse(

{title}

{description}

, { width: 1200, height: 630, }, ); }, }, }, }); ```
### Request the endpoint [#request-the-endpoint] Visit `/og-image?title=Hello&description=From%20TanStack%20Start`.
# Keyframe Animation (/docs/keyframe-animation) Takumi animates scenes two ways: * `renderAnimation()` produces an animated `webp`, `apng`, or `gif` directly. * `render()` with `timeMs` renders one frame at a time, for an animation written as an object or for external encoders like ffmpeg. Below is a keyframe animation rendered with ffmpeg + shiki for syntax highlighting: