Plot

Plot an equation you describe in plain English.

code.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 />)

Markdown source · More bulbs by samples · Typebulb home