{"spec_id":"streamline-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// streamline-basic: Basic Streamline Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Vector field: a counter-rotating vortex pair embedded in a uniform ----\n// wind. Superposed point-vortex velocity (softened near each core to avoid\n// the analytic 1/r singularity) plus a constant eastward ambient flow — the\n// combination that produces the classic \"vortex in a stream\" recirculation\n// bubble bounded by a closed separatrix.\nconst AMBIENT_WIND = 1.6;\nconst VORTICES = [\n  { x: -2, y: 0, strength: 6 },\n  { x: 2, y: 0, strength: -6 },\n];\n\nfunction velocityAt(x, y) {\n  let u = AMBIENT_WIND;\n  let v = 0;\n  for (const vortex of VORTICES) {\n    const dx = x - vortex.x;\n    const dy = y - vortex.y;\n    const rSquared = Math.max(dx * dx + dy * dy, 0.05);\n    u += (-vortex.strength * dy) / (2 * Math.PI * rSquared);\n    v += (vortex.strength * dx) / (2 * Math.PI * rSquared);\n  }\n  return [u, v];\n}\n\n// --- Trace one streamline via RK4, integrating both ways from its seed -----\nconst X_MIN = -5;\nconst X_MAX = 5;\nconst Y_MIN = -2.8;\nconst Y_MAX = 2.8;\nconst STEP = 0.035;\nconst MAX_STEPS = 480;\nconst MAX_SPEED = 14;\n\nfunction rk4Step(x, y, h) {\n  const [u1, v1] = velocityAt(x, y);\n  const [u2, v2] = velocityAt(x + (h * u1) / 2, y + (h * v1) / 2);\n  const [u3, v3] = velocityAt(x + (h * u2) / 2, y + (h * v2) / 2);\n  const [u4, v4] = velocityAt(x + h * u3, y + h * v3);\n  return [\n    x + (h * (u1 + 2 * u2 + 2 * u3 + u4)) / 6,\n    y + (h * (v1 + 2 * v2 + 2 * v3 + v4)) / 6,\n  ];\n}\n\nfunction traceHalf(x0, y0, h) {\n  const path = [];\n  let x = x0;\n  let y = y0;\n  for (let i = 0; i < MAX_STEPS; i += 1) {\n    const [u, v] = velocityAt(x, y);\n    if (Math.hypot(u, v) > MAX_SPEED || x < X_MIN || x > X_MAX || y < Y_MIN || y > Y_MAX) {\n      break;\n    }\n    path.push([x, y]);\n    [x, y] = rk4Step(x, y, h);\n  }\n  return path;\n}\n\nfunction traceStreamline(x0, y0) {\n  const forward = traceHalf(x0, y0, STEP);\n  const backward = traceHalf(x0, y0, -STEP);\n  return backward.reverse().concat(forward.slice(1));\n}\n\n// --- Seed points: an upstream row spanning the full inflow edge. Each line\n// is traced forward and backward from there, so the deflection around (and\n// recirculation bubble bounding) each vortex emerges from the field itself\n// rather than from separately-seeded core loops, which tangled into clutter.\nconst seeds = [];\nfor (let i = 0; i < 13; i += 1) {\n  seeds.push([-4.6, -2.4 + i * 0.4]);\n}\n\nconst streamlines = seeds\n  .map(([sx, sy]) => {\n    const points = traceStreamline(sx, sy);\n    const speeds = points.map(([px, py]) => Math.hypot(...velocityAt(px, py)));\n    const meanSpeed = speeds.reduce((sum, speed) => sum + speed, 0) / speeds.length;\n    return { points, meanSpeed };\n  })\n  .filter((line) => line.points.length > 4);\n\nconst speedValues = streamlines.map((line) => line.meanSpeed);\nconst speedMin = Math.min(...speedValues);\nconst speedMax = Math.max(...speedValues);\n\n// --- Imprint sequential colormap (speed → color) ----------------------------\nfunction mixHex(hexA, hexB, ratio) {\n  const a = parseInt(hexA.slice(1), 16);\n  const b = parseInt(hexB.slice(1), 16);\n  const channel = (shift) =>\n    Math.round(((a >> shift) & 255) + (((b >> shift) & 255) - ((a >> shift) & 255)) * ratio);\n  return `rgb(${channel(16)}, ${channel(8)}, ${channel(0)})`;\n}\n\n// --- Chart -------------------------------------------------------------------\n// Streamlines loop back on themselves near the vortex cores, so a plain\n// \"line\"/\"spline\" series (which auto-sorts points by ascending x) would\n// scramble the path. A \"scatter\" series with lineWidth set draws the\n// segments in data order instead, which preserves the traced curve.\n// Markers stay hidden at rest (states.hover.enabled) so a mouse-driven HTML\n// view can reveal each streamline's local speed on hover without any marker\n// clutter in the static PNG screenshot (no pointer is ever active for it).\nconst streamlineSeries = streamlines.map((line, index) => {\n  const ratio = speedMax > speedMin ? (line.meanSpeed - speedMin) / (speedMax - speedMin) : 0;\n  return {\n    type: \"scatter\",\n    name: `Streamline ${index + 1}`,\n    data: line.points,\n    color: mixHex(t.seq[0], t.seq[1], ratio),\n    lineWidth: 1.6 + ratio * 1.8,\n    meanSpeed: line.meanSpeed,\n    marker: { enabled: false, states: { hover: { enabled: true, radius: 5, lineWidth: 1 } } },\n    showInLegend: false,\n  };\n});\n\nconst legendKeySeries = [\n  {\n    type: \"scatter\",\n    name: \"Slower flow\",\n    data: [],\n    color: t.seq[0],\n    marker: { enabled: true, radius: 7, symbol: \"circle\" },\n  },\n  {\n    type: \"scatter\",\n    name: \"Faster flow\",\n    data: [],\n    color: t.seq[1],\n    marker: { enabled: true, radius: 7, symbol: \"circle\" },\n  },\n];\n\nHighcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"streamline-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Streamlines around a counter-rotating vortex pair · color and thickness encode local speed\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    title: { text: \"Distance East (km)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    min: X_MIN,\n    max: X_MAX,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    title: { text: \"Distance North (km)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    min: Y_MIN,\n    max: Y_MAX,\n    gridLineColor: t.grid,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: {\n    backgroundColor: t.elevatedBg,\n    borderColor: t.inkSoft,\n    style: { color: t.ink, fontSize: \"13px\" },\n    formatter() {\n      const speed = this.series.userOptions.meanSpeed;\n      return speed === undefined\n        ? false\n        : `<b>${this.series.name}</b><br/>speed ≈ ${speed.toFixed(2)}<br/>(${this.x.toFixed(1)}, ${this.y.toFixed(1)}) km`;\n    },\n  },\n  plotOptions: {\n    series: { animation: false, marker: { enabled: false } },\n  },\n  series: [...legendKeySeries, ...streamlineSeries],\n});\n"}