{"spec_id":"scatter-text","library":"muix","language":"javascript","code":"// anyplot.ai\n// scatter-text: Scatter Plot with Text Labels Instead of Points\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n//# anyplot-orientation: landscape\n// anyplot.ai\n// scatter-text: Scatter Plot with Text Labels Instead of Points\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-09-02\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: word clusters positioned like a t-SNE / UMAP 2D projection ------\n// Deterministic LCG so the \"organic\" jitter is reproducible across renders.\nfunction makeLcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1103515245 + 12345) % 2147483648;\n    return state / 2147483648;\n  };\n}\nconst rng = makeLcg(20260902);\n\n// Relative offsets for 8 points inside a cluster (staggered grid, no overlap).\nconst LAYOUT = [\n  [-16, 9],\n  [0, 13],\n  [16, 9],\n  [-19, -4],\n  [0, 1],\n  [19, -4],\n  [-11, -15],\n  [11, -15],\n];\nconst MAX_RADIUS = Math.max(...LAYOUT.map(([dx, dy]) => Math.hypot(dx, dy)));\n\nfunction makeCluster(id, label, color, center, words) {\n  const data = words.map((word, i) => {\n    const [dx, dy] = LAYOUT[i];\n    const jitterX = (rng() - 0.5) * 4;\n    const jitterY = (rng() - 0.5) * 4;\n    // Words nearer the cluster centroid render larger — a proximity-to-size\n    // encoding that reads as \"how representative of the cluster\" and gives\n    // each group a visual anchor instead of eight equal-weight labels.\n    const centrality = 1 - Math.hypot(dx, dy) / MAX_RADIUS;\n    return {\n      id: `${id}-${word}`,\n      x: center[0] + dx + jitterX,\n      y: center[1] + dy + jitterY,\n      label: word,\n      fontSize: Math.round(14 + 6 * centrality),\n    };\n  });\n  return { id, label, color, data };\n}\n\nconst CLUSTERS = [\n  makeCluster(\"animals\", \"Animals\", t.palette[0], [24, 76], [\n    \"tiger\",\n    \"falcon\",\n    \"dolphin\",\n    \"panther\",\n    \"sparrow\",\n    \"otter\",\n    \"lynx\",\n    \"heron\",\n  ]),\n  makeCluster(\"food\", \"Food & drink\", t.palette[1], [76, 74], [\n    \"saffron\",\n    \"espresso\",\n    \"truffle\",\n    \"paprika\",\n    \"chutney\",\n    \"brioche\",\n    \"sorbet\",\n    \"tempeh\",\n  ]),\n  makeCluster(\"tech\", \"Technology\", t.palette[2], [26, 24], [\n    \"kernel\",\n    \"compiler\",\n    \"firmware\",\n    \"latency\",\n    \"endpoint\",\n    \"container\",\n    \"pipeline\",\n    \"runtime\",\n  ]),\n  makeCluster(\"sports\", \"Sports\", t.palette[3], [78, 22], [\n    \"sprinter\",\n    \"goalie\",\n    \"referee\",\n    \"marathon\",\n    \"dugout\",\n    \"scrimmage\",\n    \"podium\",\n    \"striker\",\n  ]),\n];\n\n// Title fontsize scales linearly off the 67-char mandated-title baseline.\nconst TITLE = \"Word Embedding Projection · scatter-text · javascript · muix · anyplot.ai\";\nconst TITLE_FONT_SIZE = Math.round(22 * Math.min(1, 67 / TITLE.length));\n\n// --- Custom scatter renderer: text labels instead of point markers ---------\nfunction TextScatter(props) {\n  const { series, xScale, yScale, color } = props;\n  const points = series.data ?? [];\n  return (\n    <g>\n      {points.map((point) => (\n        <text\n          key={point.id}\n          x={xScale(point.x)}\n          y={yScale(point.y)}\n          fill={color}\n          fontSize={point.fontSize}\n          fontWeight={600}\n          textAnchor=\"middle\"\n          dominantBaseline=\"central\"\n          paintOrder=\"stroke\"\n          stroke={t.pageBg}\n          strokeWidth={point.fontSize / 4}\n          strokeLinejoin=\"round\"\n        >\n          {point.label}\n        </text>\n      ))}\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const titleHeight = 56;\n\n  return (\n    <div style={{ width, height, display: \"flex\", flexDirection: \"column\" }}>\n      <div\n        style={{\n          height: titleHeight,\n          display: \"flex\",\n          alignItems: \"center\",\n          paddingLeft: 24,\n          fontSize: TITLE_FONT_SIZE,\n          fontWeight: 600,\n          color: t.ink,\n        }}\n      >\n        {TITLE}\n      </div>\n      <ScatterChart\n        width={width}\n        height={height - titleHeight}\n        series={CLUSTERS}\n        skipAnimation\n        grid={{ vertical: true, horizontal: true }}\n        xAxis={[{ min: 0, max: 100, label: \"Embedding dimension 1\", tickNumber: 6 }]}\n        yAxis={[{ min: 0, max: 100, label: \"Embedding dimension 2\", tickNumber: 6 }]}\n        slots={{ scatter: TextScatter }}\n        slotProps={{\n          legend: { position: { vertical: \"top\", horizontal: \"right\" }, direction: \"row\" },\n        }}\n        margin={{ top: 40, right: 40, bottom: 60, left: 70 }}\n        sx={{\n          // Drop the stock axis frame/ticks for a borderless, custom look —\n          // the gridlines alone are enough reference structure.\n          \"& .MuiChartsAxis-line\": { stroke: \"none\" },\n          \"& .MuiChartsAxis-tick\": { stroke: \"none\" },\n          \"& .MuiChartsGrid-line\": { strokeOpacity: 0.5 },\n        }}\n      />\n    </div>\n  );\n}\n"}