---
format: typebulb/v1
name: Plot
---

**code.tsx**

```tsx
import React, { useEffect, useRef, useState } from "react"
import { createRoot } from "react-dom/client"
import * as echarts from "echarts"
import { compile, parse } from "mathjs"
import katex from "katex"

interface Equation {
  label: string
  expression: string
  variable: string
  range: [number, number]
  steps?: number
}

interface Insight {
  title: string
  xLabel: string
  yLabel: string
  equations: Equation[]
  explanation?: string
}

const cssVar = (name: string) =>
  getComputedStyle(document.documentElement)
    .getPropertyValue(`--chart-${name}`).trim()

const COLORS = ["#6366f1", "#f59e0b", "#10b981", "#ef4444", "#8b5cf6", "#ec4899"]

function exprToHtml(expression: string): string {
  try {
    const tex = parse(expression).toTex()
    return katex.renderToString(tex, { throwOnError: false })
  } catch {
    return expression
  }
}

function formatNum(n: number): string {
  if (n === 0) return "0"
  const abs = Math.abs(n)
  if (abs >= 0.001 && abs < 1e9)
    return n.toLocaleString(undefined, { maximumSignificantDigits: 4 })
  return n.toPrecision(4)
}

function evaluate(eq: Equation): { x: number[]; y: number[] } {
  const expr = compile(eq.expression)
  const steps = eq.steps ?? 200
  const [lo, hi] = eq.range
  const dx = (hi - lo) / steps
  const x: number[] = []
  const y: number[] = []
  for (let i = 0; i <= steps; i++) {
    const xVal = lo + i * dx
    try {
      const yVal = expr.evaluate({ [eq.variable]: xVal })
      if (typeof yVal === "number" && isFinite(yVal)) {
        x.push(xVal)
        y.push(yVal)
      }
    } catch { /* skip undefined points */ }
  }
  return { x, y }
}

function buildChartOptions(insight: Insight, width: number): echarts.EChartsOption {
  const small = width < 400

  const series: echarts.SeriesOption[] = insight.equations.map((eq, i) => {
    const { x, y } = evaluate(eq)
    return {
      name: eq.label,
      type: "line",
      showSymbol: false,
      smooth: false,
      data: x.map((xv, j) => [xv, y[j]]),
      lineStyle: { width: 2, color: COLORS[i % COLORS.length] },
      itemStyle: { color: COLORS[i % COLORS.length] },
      animationDuration: 1000,
      animationEasing: "cubicOut",
    }
  })

  return {
    backgroundColor: "transparent",
    tooltip: {
      trigger: "axis",
      formatter: (params: any) => {
        const lines = params.map((p: any) =>
          `<span style="color:${p.color}">●</span> ${p.seriesName}: <strong>${formatNum(Number(p.value[1]))}</strong>`
        )
        return `${insight.equations[0].variable} = ${formatNum(Number(params[0].value[0]))}<br/>` + lines.join("<br/>")
      },
    },
    legend: insight.equations.length > 1 ? {
      bottom: 0,
      textStyle: { color: cssVar("text-muted") },
    } : undefined,
    grid: { left: small ? "18%" : "12%", right: "5%", bottom: insight.equations.length > 1 ? "15%" : "10%", top: "10%" },
    xAxis: {
      type: "value",
      name: insight.xLabel,
      nameLocation: "center",
      nameGap: 30,
      nameTextStyle: { color: cssVar("text-muted"), fontSize: 12 },
      axisLabel: { color: cssVar("text-muted") },
      axisLine: { lineStyle: { color: cssVar("line") } },
      splitLine: { lineStyle: { color: cssVar("split"), opacity: 0.3 } },
    },
    yAxis: {
      type: "value",
      name: insight.yLabel,
      nameLocation: "center",
      nameGap: 45,
      nameTextStyle: { color: cssVar("text-muted"), fontSize: 12 },
      axisLabel: { color: cssVar("text-muted") },
      axisLine: { lineStyle: { color: cssVar("line") } },
      splitLine: { lineStyle: { color: cssVar("split"), opacity: 0.3 } },
    },
    series,
  }
}

function App() {
  const ref = useRef<HTMLDivElement>(null)
  const [insight, setInsight] = useState<Insight | null>(() => {
    try { return tb.insight<Insight>() } catch { return null }
  })
  const [isGenerating, setIsGenerating] = useState(false)
  const [error, setError] = useState<string | null>(null)

  const handleGenerate = async () => {
    setIsGenerating(true)
    setError(null)
    try {
      const result = await tb.infer<Insight>()
      if (result) {
        setInsight(result)
      }
    } catch (e: any) {
      setError(e.message ?? "Inference failed")
    } finally {
      setIsGenerating(false)
    }
  }

  useEffect(() => {
    if (!insight || !ref.current) return
    const el = ref.current
    const chart = echarts.init(el)
    const update = () => {
      try {
        chart.setOption(buildChartOptions(insight, el.offsetWidth))
        setError(null)
      } catch (e: any) {
        setError(`Chart error: ${e.message}`)
      }
    }
    update()

    const mo = new MutationObserver(update)
    mo.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] })
    const onResize = () => { chart.resize(); update() }
    window.addEventListener("resize", onResize)
    return () => { chart.dispose(); mo.disconnect(); window.removeEventListener("resize", onResize) }
  }, [insight])

  return (
    <div className="container">
      {!insight && (
        <div className="empty">
          <h1>Equation Plot</h1>
          <p>Describe a question or equation in the Data tab, then click Generate.</p>
          <button className={`generate-btn ${isGenerating ? "loading" : ""}`} onClick={handleGenerate}>
            {isGenerating ? "Generating..." : "Generate Plot"}
          </button>
        </div>
      )}
      {insight && (
        <>
          <button className={`generate-btn regen ${isGenerating ? "loading" : ""}`} onClick={handleGenerate}>
            {isGenerating ? "GENERATING..." : "PLOT NEW EQUATION"}
          </button>
          <h1 className="title">{insight.title}</h1>
          <div className="equations">
            {insight.equations.map((eq, i) => (
              <div key={i} className="eq" style={{ borderLeftColor: COLORS[i % COLORS.length] }}>
                <span className="eq-label">{eq.label}:</span>{" "}
                <span dangerouslySetInnerHTML={{ __html: exprToHtml(eq.expression) }} />
              </div>
            ))}
          </div>
          <div ref={ref} className="chart-container" />
          {insight.explanation && (
            <div className="explanation">
              <p>{insight.explanation}</p>
            </div>
          )}
        </>
      )}
      {error && <p className="error">{error}</p>}
    </div>
  )
}

createRoot(document.getElementById("root")!).render(<App />)

```
**styles.css**

```css
:root {
  --chart-text: #333;
  --chart-text-muted: #666;
  --chart-line: #ccc;
  --chart-split: #e0e0e0;
  font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

html[data-theme="dark"] {
  --chart-text: #e0e0e0;
  --chart-text-muted: #b0b0b0;
  --chart-line: #555;
  --chart-split: #333;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

.container {
  padding: 1rem;
  min-width: 320px;
  text-align: center;
}

.title {
  margin-bottom: 0.5rem;
  font-size: 1.75rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--chart-text);
}

.equations {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  justify-content: center;
  margin-bottom: 1.5rem;
}

.eq {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0.75rem;
  border-left: 3px solid #6366f1;
  background: rgba(99, 102, 241, 0.08);
  border-radius: 0 4px 4px 0;
  font-size: 0.85rem;
  color: var(--chart-text);
}

.eq-label {
  font-weight: 600;
  white-space: nowrap;
}

.eq .katex {
  font-size: 1em;
}

.chart-container {
  width: 100%;
  height: 500px;
  min-width: 320px;
}

.empty {
  padding: 3rem 1rem;
  color: var(--chart-text-muted);
}

.empty h1 {
  font-size: 2rem;
  color: var(--chart-text);
  margin-bottom: 0.5rem;
}

.empty p {
  margin-bottom: 1.5rem;
}

.generate-btn {
  background: #6366f1;
  color: #fff;
  border: none;
  padding: 0.6rem 1.5rem;
  border-radius: 6px;
  font: inherit;
  font-weight: 500;
  cursor: pointer;
  transition: opacity 0.2s;
}

.generate-btn:hover { opacity: 0.9; }
.generate-btn.loading { opacity: 0.6; cursor: wait; }
.generate-btn.regen {
  margin-bottom: 1rem;
  background: transparent;
  color: var(--chart-text-muted);
  border: 1px solid var(--chart-line);
  font-size: 0.85rem;
  padding: 0.4rem 1rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.error {
  margin-top: 1rem;
  color: #ef4444;
  font-size: 0.85rem;
}

.explanation {
  margin: 1.5rem auto 0;
  max-width: 640px;
  padding: 1rem;
  background: rgba(99, 102, 241, 0.05);
  border-radius: 8px;
  text-align: left;
  color: var(--chart-text);
  font-size: 0.95rem;
  line-height: 1.5;
  border-left: 4px solid #6366f1;
}

@media (max-width: 480px) {
  .container { padding: 0.5rem; }
  .title { font-size: 1.5rem; }
  .chart-container { height: 400px; }
}

```
**index.html**

```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css">
<script>globalThis.__VERSION__ = ""</script>
<div id="root"></div>
```
**data.txt**

```txt
Show how compound interest grows at 5%, 7%, and 10% over 30 years

```
**infer.md**

````md
Convert the user's description into one or more mathematical equations that can be plotted.

## Rules

- Output **mathjs-compatible expressions**. Use mathjs syntax: `sin(x)`, `cos(x)`, `tan(x)`, `sqrt(x)`, `abs(x)`, `log(x)` (natural log), `log(x, 10)` (base-10), `exp(x)`, `pi`, `e`, `^` for exponents.
- Each equation must use a single independent variable (usually `x` or `t`).
- Choose a sensible range for the variable that captures the interesting behavior.
- Use 200–500 steps (more for rapidly oscillating functions).
- If the description implies multiple related curves (e.g., "compare x^2 and x^3"), return multiple equations.
- If the description is a word problem, derive the equation yourself. Show the actual math, not a verbal restatement.
- Include a brief explanation of how the equations were derived or what they represent.
- Keep labels concise.

## Output Format

Return a JSON object:

```json
{
  "title": "Short descriptive title",
  "xLabel": "x-axis label with units if applicable",
  "yLabel": "y-axis label with units if applicable",
  "explanation": "Brief explanation of the equations and their significance.",
  "equations": [
    {
      "label": "Curve name",
      "expression": "mathjs expression using the variable",
      "variable": "x",
      "range": [0, 10],
      "steps": 200
    }
  ]
}

````
**insight.json**

```json
{
  "title": "Compound Interest Growth Over 30 Years",
  "xLabel": "Time (Years)",
  "yLabel": "Balance ($)",
  "explanation": "These curves illustrate the exponential growth of a $1,000 initial investment compounded annually. The higher the interest rate, the more dramatically the balance increases over time due to interest compounding on previously earned interest.",
  "equations":[
    {
      "label": "5% Interest",
      "expression": "1000 * 1.05^x",
      "variable": "x",
      "range": [0, 30],
      "steps": 300
    },
    {
      "label": "7% Interest",
      "expression": "1000 * 1.07^x",
      "variable": "x",
      "range": [0, 30],
      "steps": 300
    },
    {
      "label": "10% Interest",
      "expression": "1000 * 1.1^x",
      "variable": "x",
      "range": [0, 30],
      "steps": 300
    }
  ]
}
```
**config.json**

```json
{
  "dependencies": {
    "react": "^19.2.4",
    "react-dom": "^19.2.4",
    "echarts": "^6.0.0",
    "mathjs": "^15.1.1",
    "katex": "^0.16.33"
  },
  "description": "Plot an equation you describe in plain English.",
  "inference": {
    "title": "Generate Equation",
    "dataTitle": "Describe your equation or question",
    "submitTitle": "Generate Plot"
  }
}
```