{"spec_id":"streamline-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// streamline-basic: Basic Streamline Plot\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-09\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { LinePlot } from \"@mui/x-charts/LineChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsText } from \"@mui/x-charts/ChartsText\";\nimport { useXScale, useYScale, useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Electric dipole field: two opposite point charges. Field lines are\n// traced by RK4 arc-length integration of the *unit* field direction (not a\n// closed-form solution) — the canonical \"electric/magnetic field line\"\n// application called out in the spec. u = Ex/|E|, v = Ey/|E|. -------------\nconst CHARGE_POS = { x: -1.1, y: 0, q: 1 };\nconst CHARGE_NEG = { x: 1.1, y: 0, q: -1 };\nconst EXCLUSION_R = 0.16; // seed/terminate radius around each charge (avoids the 1/r^2 singularity)\n\nfunction fieldAt(x, y) {\n  const dxP = x - CHARGE_POS.x;\n  const dyP = y - CHARGE_POS.y;\n  const dxN = x - CHARGE_NEG.x;\n  const dyN = y - CHARGE_NEG.y;\n  const rP3 = Math.pow(dxP * dxP + dyP * dyP, 1.5) + 1e-6;\n  const rN3 = Math.pow(dxN * dxN + dyN * dyN, 1.5) + 1e-6;\n  return [\n    CHARGE_POS.q * (dxP / rP3) + CHARGE_NEG.q * (dxN / rN3),\n    CHARGE_POS.q * (dyP / rP3) + CHARGE_NEG.q * (dyN / rN3),\n  ];\n}\n\nfunction fieldStep(x, y, h) {\n  const dir = (px, py) => {\n    const [ex, ey] = fieldAt(px, py);\n    const mag = Math.hypot(ex, ey) || 1e-9;\n    return [ex / mag, ey / mag];\n  };\n  const [k1x, k1y] = dir(x, y);\n  const [k2x, k2y] = dir(x + (h / 2) * k1x, y + (h / 2) * k1y);\n  const [k3x, k3y] = dir(x + (h / 2) * k2x, y + (h / 2) * k2y);\n  const [k4x, k4y] = dir(x + h * k3x, y + h * k3y);\n  return [\n    x + (h / 6) * (k1x + 2 * k2x + 2 * k3x + k4x),\n    y + (h / 6) * (k1y + 2 * k2y + 2 * k3y + k4y),\n  ];\n}\n\nconst X_BOUND = 4.6;\nconst Y_BOUND = 2.55;\nconst STEP_LEN = 0.032;\nconst MAX_STEPS = 2200;\n\nfunction traceFieldLine(theta) {\n  let x = CHARGE_POS.x + EXCLUSION_R * Math.cos(theta);\n  let y = CHARGE_POS.y + EXCLUSION_R * Math.sin(theta);\n  const xs = [x];\n  const ys = [y];\n  const mags = [Math.hypot(...fieldAt(x, y))];\n  for (let i = 0; i < MAX_STEPS; i++) {\n    [x, y] = fieldStep(x, y, STEP_LEN);\n    const dSink = Math.hypot(x - CHARGE_NEG.x, y - CHARGE_NEG.y);\n    if (dSink < EXCLUSION_R) {\n      const snap = EXCLUSION_R / dSink;\n      x = CHARGE_NEG.x + (x - CHARGE_NEG.x) * snap;\n      y = CHARGE_NEG.y + (y - CHARGE_NEG.y) * snap;\n      xs.push(x);\n      ys.push(y);\n      mags.push(Math.hypot(...fieldAt(x, y)));\n      break;\n    }\n    if (Math.abs(x) > X_BOUND || Math.abs(y) > Y_BOUND) break;\n    xs.push(x);\n    ys.push(y);\n    mags.push(Math.hypot(...fieldAt(x, y)));\n  }\n  return { xs, ys, mags };\n}\n\n// 16 seeds evenly spaced around the positive charge, offset by half a step so\n// none lands on the +/-x axis (the exact axis line never curves back and\n// would otherwise run to the domain edge as a degenerate special case).\nconst LINE_COUNT = 16;\nconst rawLines = Array.from({ length: LINE_COUNT }, (_, k) =>\n  traceFieldLine(((k + 0.5) / LINE_COUNT) * 2 * Math.PI),\n);\n\n// --- Color-by-magnitude: MUI X has no per-vertex line coloring, so each\n// field line is chopped into fixed sub-segments (own xAxisId + series) and\n// each segment gets one solid color from imprint_seq, driven by its average\n// log-magnitude — a discretized stand-in for a continuous \"speed\" colormap.\nconst allLogMags = rawLines.flatMap((l) => l.mags.map((m) => Math.log10(m + 1e-6)));\nconst LOG_MIN = Math.min(...allLogMags);\nconst LOG_MAX = Math.max(...allLogMags);\n\nfunction hexToRgb(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\nfunction lerpColor(hexA, hexB, f) {\n  const a = hexToRgb(hexA);\n  const b = hexToRgb(hexB);\n  const c = a.map((v, i) => Math.round(v + (b[i] - v) * Math.min(1, Math.max(0, f))));\n  return `rgb(${c[0]}, ${c[1]}, ${c[2]})`;\n}\n\nconst SEGMENTS_PER_LINE = 5;\nlet axisCounter = 0;\nconst segments = [];\nconst arrowSpecs = [];\n\nfor (const line of rawLines) {\n  const n = line.xs.length;\n  const segLen = Math.max(1, Math.floor((n - 1) / SEGMENTS_PER_LINE));\n  const lineSegments = [];\n  for (let s = 0; s < SEGMENTS_PER_LINE; s++) {\n    const start = s * segLen;\n    const end = s === SEGMENTS_PER_LINE - 1 ? n - 1 : (s + 1) * segLen;\n    if (end <= start) continue;\n    const segMags = line.mags.slice(start, end + 1);\n    const avgLogMag = segMags.reduce((acc, m) => acc + Math.log10(m + 1e-6), 0) / segMags.length;\n    const norm = LOG_MAX > LOG_MIN ? (avgLogMag - LOG_MIN) / (LOG_MAX - LOG_MIN) : 0.5;\n    const segment = {\n      axisId: `fl-${axisCounter++}`,\n      xs: line.xs.slice(start, end + 1),\n      ys: line.ys.slice(start, end + 1),\n      color: lerpColor(t.seq[0], t.seq[1], norm),\n    };\n    segments.push(segment);\n    lineSegments.push(segment);\n  }\n  // Direction arrows at two points along the raw path, colored to match the\n  // local field-strength segment they fall in.\n  if (n > 12 && lineSegments.length > 0) {\n    [0.15, 0.42, 0.72].forEach((frac) => {\n      const idx = Math.min(n - 3, Math.max(2, Math.round(frac * (n - 1))));\n      const segIdx = Math.min(lineSegments.length - 1, Math.floor(idx / segLen));\n      const segment = lineSegments[segIdx];\n      arrowSpecs.push({\n        x0: line.xs[idx],\n        y0: line.ys[idx],\n        x1: line.xs[idx + 2],\n        y1: line.ys[idx + 2],\n        color: segment.color,\n      });\n    });\n  }\n}\n\nconst SHARED_AXIS_ID = segments[0].axisId; // every xAxis shares the same explicit\n// [X_MIN, X_MAX] linear domain, so any one of them maps data->pixels for all lines.\n\n// Display domain is tighter than the integration bound X_BOUND on the right:\n// this dipole's field lines either loop between the two charges or escape\n// toward -x, so nothing ever occupies the right two-thirds of a symmetric\n// domain — cropping it there fills the canvas instead of framing empty space.\nconst X_MIN = -X_BOUND;\nconst X_MAX = 2.0;\nconst Y_MIN = -Y_BOUND;\nconst Y_MAX = Y_BOUND;\n\nconst TITLE = \"Electric Dipole Field Lines · streamline-basic · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 70;\nconst TITLE_FONT_SIZE = Math.max(15, Math.round(22 * Math.min(1, 67 / TITLE.length)));\n\nconst MARGIN = { top: 30, bottom: 90, left: 100, right: 60 };\n\nfunction arrowPoints(cx, cy, angle, size) {\n  const backAngle = angle + Math.PI;\n  const spread = 0.48;\n  const tip = [cx + Math.cos(angle) * size, cy + Math.sin(angle) * size];\n  const left = [cx + Math.cos(backAngle - spread) * size, cy + Math.sin(backAngle - spread) * size];\n  const right = [cx + Math.cos(backAngle + spread) * size, cy + Math.sin(backAngle + spread) * size];\n  return `${tip.join(\",\")} ${left.join(\",\")} ${right.join(\",\")}`;\n}\n\n// --- Custom overlay: charge markers, direction arrowheads, and a manual\n// field-strength gradient legend (community x-charts has no bound color-axis\n// legend wired for a line chart, so the swatch is drawn directly). ----------\nfunction FieldOverlay() {\n  const xScale = useXScale(SHARED_AXIS_ID);\n  const yScale = useYScale();\n  const area = useDrawingArea();\n\n  const posPx = { x: xScale(CHARGE_POS.x), y: yScale(CHARGE_POS.y) };\n  const negPx = { x: xScale(CHARGE_NEG.x), y: yScale(CHARGE_NEG.y) };\n\n  const legendW = 190;\n  const legendH = 14;\n  const legendX = area.left + area.width - legendW - 18;\n  const legendY = area.top + 16;\n\n  return (\n    <g>\n      {arrowSpecs.map((a, i) => {\n        const p0 = { x: xScale(a.x0), y: yScale(a.y0) };\n        const p1 = { x: xScale(a.x1), y: yScale(a.y1) };\n        const angle = Math.atan2(p1.y - p0.y, p1.x - p0.x);\n        return (\n          <polygon\n            key={i}\n            points={arrowPoints(p0.x, p0.y, angle, 11)}\n            fill={a.color}\n            stroke={t.pageBg}\n            strokeWidth={1}\n          />\n        );\n      })}\n\n      <circle cx={posPx.x} cy={posPx.y} r={17} fill={t.ink} stroke={t.pageBg} strokeWidth={2.5} />\n      <ChartsText\n        x={posPx.x}\n        y={posPx.y}\n        text=\"+\"\n        style={{ fontSize: 20, fontWeight: 700, fill: t.pageBg, textAnchor: \"middle\", dominantBaseline: \"central\" }}\n      />\n      <circle cx={negPx.x} cy={negPx.y} r={17} fill={t.ink} stroke={t.pageBg} strokeWidth={2.5} />\n      <ChartsText\n        x={negPx.x}\n        y={negPx.y}\n        text=\"−\"\n        style={{ fontSize: 20, fontWeight: 700, fill: t.pageBg, textAnchor: \"middle\", dominantBaseline: \"central\" }}\n      />\n\n      <defs>\n        <linearGradient id=\"streamline-strength-grad\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">\n          <stop offset=\"0%\" stopColor={t.seq[0]} />\n          <stop offset=\"100%\" stopColor={t.seq[1]} />\n        </linearGradient>\n      </defs>\n      <ChartsText\n        x={legendX + legendW / 2}\n        y={legendY - 14}\n        text=\"Field strength\"\n        style={{ fontSize: 14, fill: t.inkSoft, textAnchor: \"middle\", dominantBaseline: \"central\" }}\n      />\n      <rect x={legendX} y={legendY} width={legendW} height={legendH} rx={3} fill=\"url(#streamline-strength-grad)\" />\n      <ChartsText\n        x={legendX}\n        y={legendY + legendH + 14}\n        text=\"weak\"\n        style={{ fontSize: 13, fill: t.inkSoft, textAnchor: \"start\", dominantBaseline: \"central\" }}\n      />\n      <ChartsText\n        x={legendX + legendW}\n        y={legendY + legendH + 14}\n        text=\"strong\"\n        style={{ fontSize: 13, fill: t.inkSoft, textAnchor: \"end\", dominantBaseline: \"central\" }}\n      />\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  return (\n    <div\n      style={{\n        width: window.ANYPLOT_SIZE.width,\n        height: window.ANYPLOT_SIZE.height,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <div\n        style={{\n          height: TITLE_HEIGHT,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontSize: TITLE_FONT_SIZE,\n          fontWeight: 500,\n          color: t.ink,\n        }}\n      >\n        {TITLE}\n      </div>\n      <ChartContainer\n        width={window.ANYPLOT_SIZE.width}\n        height={window.ANYPLOT_SIZE.height - TITLE_HEIGHT}\n        margin={MARGIN}\n        skipAnimation\n        sx={{\n          \".MuiLineElement-root\": { strokeWidth: 2.75, strokeLinecap: \"round\" },\n          \"& .MuiChartsGrid-line\": { stroke: t.grid, strokeWidth: 0.75 },\n        }}\n        xAxis={segments.map((seg) => ({\n          id: seg.axisId,\n          scaleType: \"linear\",\n          data: seg.xs,\n          min: X_MIN,\n          max: X_MAX,\n        }))}\n        yAxis={[{ scaleType: \"linear\", min: Y_MIN, max: Y_MAX }]}\n        series={segments.map((seg) => ({\n          type: \"line\",\n          data: seg.ys,\n          xAxisId: seg.axisId,\n          color: seg.color,\n          curve: \"linear\",\n          showMark: false,\n        }))}\n      >\n        <ChartsGrid horizontal />\n        <LinePlot />\n        <FieldOverlay />\n        <ChartsXAxis\n          axisId={SHARED_AXIS_ID}\n          label=\"x (normalized distance)\"\n          labelStyle={{ fontSize: 16, fill: t.ink, fontWeight: 500 }}\n          tickLabelStyle={{ fontSize: 14, fill: t.inkSoft }}\n          stroke={t.inkSoft}\n        />\n        <ChartsYAxis\n          label=\"y (normalized distance)\"\n          labelStyle={{ fontSize: 16, fill: t.ink, fontWeight: 500 }}\n          tickLabelStyle={{ fontSize: 14, fill: t.inkSoft }}\n          stroke={t.inkSoft}\n        />\n      </ChartContainer>\n    </div>\n  );\n}\n"}