# How to generate charts from LLM-written code in a sandbox Run the model's plotting code in a sandbox's code interpreter. Take each chart back as a PNG result, or as an SVG or PDF file that the code saved. **On Runtime, 10,000 charts cost about $2.71** on 2 vCPU, 4 GiB sandboxes that each run 30 seconds. The figure uses the rates in effect on 26 October 2026. The reason is that the sandbox is billed for the CPU the plotting uses. matplotlib 3.6.3, pandas and NumPy are in the default image. A matplotlib figure comes back from the interpreter as a PNG, with no file handling at all. ## The short answer Give the model one tool that runs Python. Pick the images out of the cell's results: ```ts check import { writeFile } from "node:fs/promises"; import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create({ network: { internet: false }, timeoutSeconds: 600 }); // The code below is what the model wrote. const cell = await sbx.interpreter.run( [ "import matplotlib.pyplot as plt", "months = ['Jan', 'Feb', 'Mar', 'Apr']", "signups = [120, 180, 150, 240]", "plt.figure(figsize=(6, 3.5))", "plt.plot(months, signups, marker='o')", "plt.title('Sign-ups by month')", "plt.show()", ].join("\n"), { timeoutMs: 30_000 }, ); let n = 0; for (const result of cell.results) { const png = result.data["image/png"]; if (typeof png === "string") await writeFile(`chart-${n++}.png`, Buffer.from(png, "base64")); } console.log(cell.status, cell.error?.value ?? "no error", `${n} chart(s)`); ``` ```python check import base64 from withruntime import Sandbox with Sandbox.create(network={"internet": False}, timeout_seconds=600) as sbx: cell = sbx.interpreter.run( "\n".join([ "import matplotlib.pyplot as plt", "months = ['Jan', 'Feb', 'Mar', 'Apr']", "signups = [120, 180, 150, 240]", "plt.figure(figsize=(6, 3.5))", "plt.plot(months, signups, marker='o')", "plt.title('Sign-ups by month')", "plt.show()", ]), timeout_ms=30_000, ) charts = [r["data"]["image/png"] for r in cell["results"] if "image/png" in r["data"]] for n, png in enumerate(charts): with open(f"chart-{n}.png", "wb") as file: file.write(base64.b64decode(png)) print(cell["status"], cell["error"], len(charts), "chart(s)") ``` Collect every PNG in the results, because one cell can draw more than one chart. Send the model the cell's `stdout` and error, not the image bytes. Show the images to the user. ## When the model's chart fails Model-written plotting code fails in predictable ways. The cell reports which failure occurred, so the model can fix it on the next turn: | What went wrong | What comes back | What to send the model | | ------------------------------- | ---------------------------------------------------- | ------------------------------- | | A typo or a wrong column name | `status: "error"`, with the error name and traceback | The error value and last lines | | A plot that loops or never ends | `status: "timeout"` after `timeoutMs` | "The cell timed out after 30 s" | | No figure drawn | `status: "ok"`, no `image/png` result | "No chart was produced" | | A library the image lacks | `ModuleNotFoundError` in the error | Nothing: add it to the image | Because interpreter variables persist, the model's second attempt can reuse the data it already loaded instead of starting over. ## Vector charts and other formats A PNG suits a chat reply. For a report or a slide, have the code save the file, then read it back: ```ts check import { writeFile } from "node:fs/promises"; import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create({ network: { internet: false } }); await sbx.interpreter.run( "import matplotlib\nmatplotlib.use('Agg')\nimport matplotlib.pyplot as plt\n" + "plt.bar(['north', 'south'], [42, 17])\n" + "plt.savefig('/workspace/chart.svg')\nplt.savefig('/workspace/chart.pdf')", ); await writeFile("chart.svg", await sbx.files.readText("/workspace/chart.svg")); await writeFile("chart.pdf", await sbx.files.read("/workspace/chart.pdf")); ``` An SVG is text, so it can go straight into an HTML page. The interpreter also runs R. R plots come back as PNG in the same way. R installs the first time a sandbox uses it. That took about 90 seconds on 23 September 2026. To skip that wait, bake R into an image. ## More plotting libraries The default image includes matplotlib. Put anything else your charts need into a custom image once. Every sandbox then starts with it: ```python check from withruntime import Runtime runtime = Runtime() runtime.images.build(name="charts", recipe={"pip": ["seaborn", "plotly"]}) with runtime.sandboxes.create(image="charts", network={"internet": False}) as sbx: cell = sbx.interpreter.run("import seaborn\nseaborn.__version__") print(cell["results"][0]["data"]["text/plain"]) ``` Building an image is free. A stored image costs $0.08 per GB per 30-day month. An account without credit stores the first three images free ([custom images](/docs/images)). ## Keep the chart code away from everything else The model decides what code runs, and the data may belong to a customer. The sandbox is a Firecracker microVM with its own kernel, so a bad cell cannot reach your servers. With the internet off, the code cannot send the data anywhere. Root in the guest cannot turn the network back on. For a chatbot where one conversation makes many charts, keep one sandbox per conversation with `Sandbox.getOrCreate(name, { idlePauseSeconds: 300 })`. Its data then stays loaded between questions, and the sandbox pauses when the user goes quiet. ## What a chart-generation feature needs | Need | How Runtime covers it | | ------------------------- | --------------------------------------------------------------------- | | Plotting in Python | matplotlib 3.6.3, pandas and NumPy in the default image | | Charts back without files | Figures return as base64 PNG results from `interpreter.run` | | Vector output | `savefig` to SVG or PDF, then `files.readText` or `files.read` | | R graphics | The interpreter's R language; plots return as PNG | | Errors the model can fix | Error name, value and traceback on each cell | | Bounded run time | `timeoutMs` per cell | | From a terminal | `runtime sandboxes run-code plot.py --out-dir charts` saves PNGs | ## What it costs Assume 10,000 chart requests a month. Each request runs in a 2 vCPU, 4 GiB sandbox for 30 seconds and uses 3 CPU-seconds to draw: ``` CPU: 10,000 × 3 s / 3,600 × $0.025 = $0.21 Memory: 10,000 × 30 s / 3,600 × 4 GiB × $0.0075 = $2.50 Total: $2.71 ``` Memory is most of the bill. So a chart sandbox with 2 GiB halves the memory line. Runtime charges $0.025 per vCPU-hour of measured CPU and $0.0075 per GiB-hour of memory ([pricing](/docs/pricing)). ## Start ```bash no-run npx withruntime sandboxes run -- python3 -c 'import matplotlib; print(matplotlib.__version__)' ``` Open the link in your browser to approve the first run. Then `runtime sandboxes run-code plot.py --out-dir charts` writes each figure that a script draws to the `charts` folder ([CLI](/docs/cli)). Related: [data analysis agent](/use-cases/data-analysis-agent), [code interpreter for chatbots](/use-cases/code-interpreter-for-chatbots), [spreadsheet analysis](/use-cases/spreadsheet-analysis), [what a code interpreter is](/glossary/code-interpreter). Facts on this page were checked on 26 October 2026.