Plot an equation you describe in plain English.
---
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/[email protected]/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"
}
}
```