{"spec_id":"bubble-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// bubble-basic: Basic Bubble Chart\n// Library: highcharts 12.6.0 | JavaScript 22.23.3\n// Quality: 82/100 | Updated: 2026-09-30\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\n// Market analysis: growth rate vs. revenue, bubble size = revenue share of segment.\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst X_MIN = -5;\nconst X_MAX = 30;\nconst Y_MIN = 10;\nconst Y_MAX = 500;\nconst Z_MIN = 10;\nconst Z_MAX = 100;\nconst R_MIN = 9;\nconst R_MAX = 32;\n\n// Scale by area, not radius, so bubble size reads proportionally.\nfunction radiusForShare(share) {\n  const frac = Math.max(0, Math.min(1, (share - Z_MIN) / (Z_MAX - Z_MIN)));\n  return R_MIN + (R_MAX - R_MIN) * Math.sqrt(frac);\n}\n\n// Small bubbles have the least area to carry color, so nudge their opacity up\n// a bit to keep them from washing out against the plot background.\nfunction alphaForShare(share) {\n  const frac = Math.max(0, Math.min(1, (share - Z_MIN) / (Z_MAX - Z_MIN)));\n  return 0.6 + 0.15 * (1 - frac);\n}\n\n// Rough plot-area pixel box (mount minus title/axis/legend margins), used only\n// to space out bubbles below — it doesn't need to match the real layout exactly.\nconst PLOT_W_PX = 1300;\nconst PLOT_H_PX = 700;\n\n// A candidate is rejected if it would land within 1+ neighbor's combined\n// radius: a single near-total overlap already hides the smaller bubble behind\n// the larger one, so any close neighbor triggers a resample, not just a\n// 2+-way clump. The distance threshold itself (1.0x combined radius) allows\n// partial edge-touching overlap through — only near-total fusion is retried.\nfunction closeNeighborCount(candidate, placed) {\n  let count = 0;\n  for (const p of placed) {\n    const dxPx = ((candidate.growthRate - p.growthRate) / (X_MAX - X_MIN)) * PLOT_W_PX;\n    const dyPx = ((candidate.revenue - p.revenue) / (Y_MAX - Y_MIN)) * PLOT_H_PX;\n    const dist = Math.hypot(dxPx, dyPx);\n    if (dist < (candidate.r + p.r) * 1.0) count += 1;\n  }\n  return count;\n}\n\nconst companies = [];\nfor (let i = 0; i < 70; i += 1) {\n  let candidate;\n  for (let attempt = 0; attempt < 10; attempt += 1) {\n    const growthRate = X_MIN + rand() * (X_MAX - X_MIN);\n    const revenue = Y_MIN + rand() * (Y_MAX - Y_MIN);\n    // Each company's share is relative to its own market segment, not a\n    // shared market — so these values do NOT sum to 100 across companies,\n    // unlike a conventional \"market share\" metric. Labelled as such below.\n    const segmentShare = Z_MIN + rand() * (Z_MAX - Z_MIN);\n    candidate = { growthRate, revenue, segmentShare, r: radiusForShare(segmentShare) };\n    if (attempt === 9 || closeNeighborCount(candidate, companies) < 1) break;\n  }\n  companies.push(candidate);\n}\n\nconst [fr, fg, fb] = [1, 3, 5].map((i) => parseInt(t.palette[0].slice(i, i + 2), 16));\nconst fillForShare = (share) => `rgba(${fr}, ${fg}, ${fb}, ${alphaForShare(share)})`;\nconst seriesData = companies.map((c) => ({\n  x: c.growthRate,\n  y: c.revenue,\n  marker: {\n    radius: c.r,\n    fillColor: fillForShare(c.segmentShare),\n    // Page-bg stroke carves a visible edge between overlapping same-color\n    // bubbles instead of them reading as one merged blob.\n    lineColor: t.pageBg,\n    lineWidth: 3,\n  },\n  custom: { segmentShare: Math.round(c.segmentShare) },\n}));\n\n// --- Chart post-render helpers ------------------------------------------------\nfunction drawSizeLegend(chart) {\n  // Highcharts core has no bubbleLegend (that lives in highcharts-more), so\n  // the size key is drawn manually in the reserved right margin, vertically\n  // centered in the plot area so leftover space splits evenly top/bottom.\n  const legendX = chart.plotLeft + chart.plotWidth + 40;\n  const rowHeight = 2 * R_MAX + 16;\n  const titleHeight = 54;\n  const totalHeight = titleHeight + 3 * rowHeight;\n  let cursorY = chart.plotTop + Math.max(0, (chart.plotHeight - totalHeight) / 2);\n\n  chart.renderer\n    .text(\"Share of Own<br/>Market Segment (%)\", legendX, cursorY, true)\n    .css({ color: t.ink, fontSize: \"15px\", fontWeight: \"600\" })\n    .add();\n  cursorY += titleHeight;\n\n  [Z_MIN, (Z_MIN + Z_MAX) / 2, Z_MAX].forEach((share) => {\n    const r = radiusForShare(share);\n    const cy = cursorY + R_MAX;\n    chart.renderer\n      .circle(legendX + R_MAX, cy, r)\n      .attr({ fill: fillForShare(share), stroke: t.palette[0], \"stroke-width\": 1.2 })\n      .add();\n    chart.renderer\n      .text(`${Math.round(share)}%`, legendX + 2 * R_MAX + 16, cy + 5)\n      .css({ color: t.inkSoft, fontSize: \"14px\" })\n      .add();\n    cursorY += rowHeight;\n  });\n}\n\n// --- Chart -------------------------------------------------------------------\n// Core bundle has no highcharts-more, so bubbles are core \"scatter\" points\n// with a per-point marker.radius (area-scaled) instead of the \"bubble\" series\n// type — same visual result using only the loaded core module.\nHighcharts.chart(\n  \"container\",\n  {\n    chart: {\n      type: \"scatter\",\n      backgroundColor: \"transparent\",\n      animation: false,\n      marginRight: 210,\n      style: { fontFamily: \"inherit\" },\n    },\n    credits: { enabled: false },\n    colors: t.palette,\n    title: {\n      text: \"bubble-basic · javascript · highcharts · anyplot.ai\",\n      style: { color: t.ink, fontSize: \"23px\", fontWeight: \"700\" },\n      margin: 26,\n    },\n    xAxis: {\n      title: {\n        text: \"Year-over-Year Growth Rate (%)\",\n        style: { color: t.inkSoft, fontSize: \"16px\", fontWeight: \"500\" },\n      },\n      lineColor: t.inkSoft,\n      tickColor: t.inkSoft,\n      tickWidth: 0,\n      tickLength: 0,\n      gridLineColor: t.grid,\n      gridLineWidth: 1,\n      labels: { style: { color: t.inkSoft, fontSize: \"14px\" }, format: \"{value}%\" },\n    },\n    yAxis: {\n      title: {\n        text: \"Annual Revenue ($M)\",\n        style: { color: t.inkSoft, fontSize: \"16px\", fontWeight: \"500\" },\n      },\n      lineColor: t.inkSoft,\n      tickColor: t.inkSoft,\n      tickWidth: 0,\n      tickLength: 0,\n      gridLineColor: t.grid,\n      gridLineWidth: 1,\n      labels: { style: { color: t.inkSoft, fontSize: \"14px\" }, format: \"${value}\" },\n    },\n    legend: { enabled: false },\n    tooltip: {\n      backgroundColor: t.elevatedBg,\n      borderColor: t.grid,\n      style: { color: t.ink, fontSize: \"13px\" },\n      pointFormatter: function pointFormatter() {\n        return (\n          `Growth: <b>${this.x.toFixed(1)}%</b><br/>` +\n          `Revenue: <b>$${this.y.toFixed(0)}M</b><br/>` +\n          `Share of own segment: <b>${this.custom.segmentShare}%</b>`\n        );\n      },\n    },\n    plotOptions: {\n      series: { animation: false },\n      scatter: { marker: { symbol: \"circle\", states: { hover: { lineWidthPlus: 1 } } } },\n    },\n    series: [{ name: \"Companies\", data: seriesData, showInLegend: false }],\n  },\n  function drawExtras(chart) {\n    drawSizeLegend(chart);\n  },\n);\n"}