{"spec_id":"scatter-annotated","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// scatter-annotated: Annotated Scatter Plot with Text Labels\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Fictional startups: annual revenue vs. valuation, both in $ millions.\n// A tiny fixed-seed LCG stands in for `Math.random()` (not reproducible in the\n// browser) so re-running the snippet always draws the same jitter.\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1103515245 + 12345) % 2147483648;\n  return seed / 2147483648;\n}\n\nconst companies = [\n  \"Nimbusly\", \"Quantara\", \"Fernwave\", \"Ledgerly\", \"Brightloom\",\n  \"Cursive AI\", \"Solstice Bio\", \"Meadowstack\", \"Vantage Grid\", \"Pixelforge\",\n  \"Cobalt Route\", \"Driftline\", \"Harborlytics\", \"Kelvin Labs\", \"Origami Cloud\",\n  \"Tundra Works\",\n];\nconst revenue = [\n  8, 42, 15, 95, 28, 6, 61, 33, 110, 19, 47, 12, 75, 24, 55, 88,\n];\n// Two indices are hand-placed genuine outliers rather than trend + noise:\n// Origami Cloud (index 14) commands a valuation multiple far above its\n// revenue-implied trend, while Tundra Works (index 15) — despite leading\n// revenue among its peers — trades at a steep discount. Both stand out\n// visibly from the otherwise near-linear cloud, earning their labels.\nconst OUTLIER_VALUATIONS = { 14: 340, 15: 125 };\nconst valuation = revenue.map((r, i) => {\n  if (i in OUTLIER_VALUATIONS) return OUTLIER_VALUATIONS[i];\n  const noise = (lcg() - 0.5) * 50;\n  return Math.max(5, r * 3.4 + noise);\n});\nconst points = companies.map((name, i) => ({\n  x: revenue[i],\n  y: valuation[i],\n  label: name,\n  outlier: i in OUTLIER_VALUATIONS,\n}));\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Label placement ----------------------------------------------------------\n// Fan each label outward from the point cloud's pixel centroid, then run a\n// greedy de-collision pass: labels are placed in x-order, and any label whose\n// box would overlap an already-placed one is nudged upward until it clears.\nconst LEADER_PX = 26;\n\nconst pointLabelsPlugin = {\n  id: \"pointLabels\",\n  afterDatasetsDraw(chart) {\n    const { ctx } = chart;\n    const meta = chart.getDatasetMeta(0);\n    ctx.save();\n    ctx.textBaseline = \"middle\";\n\n    const centroidPx = meta.data.reduce(\n      (acc, p) => ({ x: acc.x + p.x / meta.data.length, y: acc.y + p.y / meta.data.length }),\n      { x: 0, y: 0 },\n    );\n\n    const LABEL_HEIGHT = 19;\n    const placements = meta.data.map((point, i) => {\n      const dx = point.x - centroidPx.x;\n      const dirX = dx >= 0 ? 1 : -1;\n      const align = dirX >= 0 ? \"left\" : \"right\";\n      const label = points[i].label;\n      const outlier = points[i].outlier;\n      const font = outlier ? \"700 15px sans-serif\" : \"500 14px sans-serif\";\n      ctx.font = font;\n      return {\n        point,\n        label,\n        align,\n        outlier,\n        font,\n        width: ctx.measureText(label).width,\n        x: point.x + dirX * LEADER_PX,\n        y: point.y - LEADER_PX,\n      };\n    });\n\n    placements.sort((a, b) => a.point.x - b.point.x);\n    for (let i = 1; i < placements.length; i++) {\n      const a = placements[i];\n      const ax0 = a.align === \"left\" ? a.x : a.x - a.width;\n      const ax1 = ax0 + a.width;\n      for (let j = 0; j < i; j++) {\n        const b = placements[j];\n        const bx0 = b.align === \"left\" ? b.x : b.x - b.width;\n        const bx1 = bx0 + b.width;\n        const overlapX = ax0 < bx1 && ax1 > bx0;\n        const overlapY = Math.abs(a.y - b.y) < LABEL_HEIGHT;\n        if (overlapX && overlapY) a.y = b.y - LABEL_HEIGHT;\n      }\n    }\n\n    placements.forEach((p) => {\n      // Thin leader line from the marker edge to the label anchor.\n      ctx.strokeStyle = t.inkSoft;\n      ctx.globalAlpha = 0.5;\n      ctx.lineWidth = 1;\n      ctx.beginPath();\n      ctx.moveTo(p.point.x, p.point.y);\n      ctx.lineTo(p.x, p.y + 4);\n      ctx.stroke();\n\n      ctx.globalAlpha = 1;\n      ctx.fillStyle = t.ink;\n      ctx.font = p.font;\n      ctx.textAlign = p.align;\n      ctx.fillText(p.label, p.x + (p.align === \"left\" ? 6 : -6), p.y);\n    });\n    ctx.restore();\n  },\n};\n\n// --- Chart ---------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      {\n        label: \"Startups\",\n        data: points,\n        // Outliers render at full opacity and a larger radius so they stand\n        // out from the ~70%-alpha trend cloud they deviate from.\n        backgroundColor: points.map((p) => (p.outlier ? t.palette[0] : `${t.palette[0]}B3`)),\n        borderColor: t.pageBg,\n        borderWidth: 1.5,\n        pointRadius: points.map((p) => (p.outlier ? 13 : 9)),\n        pointHoverRadius: points.map((p) => (p.outlier ? 13 : 9)),\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 40, right: 90, bottom: 10, left: 10 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"scatter-annotated · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { bottom: 20 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        title: { display: true, text: \"Annual Revenue ($M)\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        border: { color: t.inkSoft },\n      },\n      y: {\n        type: \"linear\",\n        title: { display: true, text: \"Valuation ($M)\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        border: { color: t.inkSoft },\n      },\n    },\n  },\n  plugins: [pointLabelsPlugin],\n});\n"}