{"spec_id":"scatter-text","library":"makie","language":"julia","code":"# anyplot.ai\n# scatter-text: Scatter Plot with Text Labels Instead of Points\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 95/100 | Created: 2026-09-02\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# --- Theme tokens -------------------------------------------------------------\nconst THEME    = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG  = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst INK      = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\n\n# Imprint palette — 8 hues, theme-independent, hybrid-v3 sort\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\", colorant\"#C475FD\", colorant\"#4467A3\", colorant\"#BD8233\",\n    colorant\"#AE3030\", colorant\"#2ABCCD\", colorant\"#954477\", colorant\"#99B314\",\n]\n\n# --- Data -----------------------------------------------------------------\n# Fictional companies positioned by revenue growth vs. profit margin —\n# competitive-landscape mapping where the company name matters more than density.\nsectors = [\"Technology\", \"Healthcare\", \"Finance\", \"Energy\"]\n\ncompanies = Dict(\n    \"Technology\" => [\"NovaTech\", \"ByteForge\", \"QuantumEdge\", \"SiliconLoop\", \"CloudSpire\", \"DataForge\", \"PixelWorks\"],\n    \"Healthcare\" => [\"VitalCure\", \"BioNova\", \"MediSphere\", \"PulseCare\", \"GenomeWorks\", \"CarePoint\", \"TheraLink\"],\n    \"Finance\" => [\"CapitalArc\", \"TrustBridge\", \"LedgerPeak\", \"FiscalCore\", \"AssetWave\", \"VaultStream\", \"PrimeYield\"],\n    \"Energy\" => [\"SolarPeak\", \"WindForge\", \"HydroCore\", \"GeoVolt\", \"EcoGrid\", \"TerraPower\", \"BrightFuel\"],\n)\n\n# (growth_mean, growth_std, margin_mean, margin_std) per sector.\n# Healthcare is drawn with a tighter spread on purpose: it is the one sector\n# where labels sit close enough together to require the density-management\n# techniques (rotation jitter + alpha) the spec calls out for dense regions.\ncluster_params = Dict(\n    \"Technology\" => (28.0, 6.0, 18.0, 5.0),\n    \"Healthcare\" => (14.0, 2.5, 24.0, 2.8),\n    \"Finance\" => (6.0, 5.0, 22.0, 5.5),\n    \"Energy\" => (10.0, 7.0, 10.0, 6.0),\n)\n\nlabels = String[]\ngrowth = Float64[]\nmargin = Float64[]\npoint_colors = RGB{Float64}[]\npoint_sectors = String[]\n\nfor (i, sector) in enumerate(sectors)\n    growth_mean, growth_std, margin_mean, margin_std = cluster_params[sector]\n    for name in companies[sector]\n        push!(labels, name)\n        push!(growth, growth_mean + growth_std * randn())\n        push!(margin, margin_mean + margin_std * randn())\n        push!(point_colors, IMPRINT_PALETTE[i])\n        push!(point_sectors, sector)\n    end\nend\n\n# De-overlap pass: the tight Healthcare cluster can draw a label almost on top\n# of a neighbor (in or out of Healthcare) by chance — nudge any such pair\n# apart symmetrically along their connecting vector. Runs a few passes since\n# separating one pair can nudge a label into a third; only Healthcare's\n# tighter cluster is normalized this aggressively, so other sectors keep\n# their original (already-reviewed) spacing untouched.\nhealthcare_idx = findall(==(\"Healthcare\"), point_sectors)\nxspan, yspan = 48.0, 38.0  # matches xlims!/ylims! below\nmin_norm_dist = 0.05\nn = length(labels)\nfor _pass in 1:4, a in 1:n, b in (a + 1):n\n    if a ∉ healthcare_idx && b ∉ healthcare_idx\n        continue\n    end\n    dx = (growth[b] - growth[a]) / xspan\n    dy = (margin[b] - margin[a]) / yspan\n    dist = max(hypot(dx, dy), 1e-6)\n    if dist < min_norm_dist\n        push_x = (min_norm_dist - dist) * (dx / dist) * xspan / 2\n        push_y = (min_norm_dist - dist) * (dy / dist) * yspan / 2\n        growth[a] -= push_x; margin[a] -= push_y\n        growth[b] += push_x; margin[b] += push_y\n    end\nend\n\n# CloudSpire is the clear growth outlier — give it visual emphasis (larger,\n# bolder label) instead of leaving it to blend in with the rest of the cluster.\noutlier_idx = findfirst(==(\"CloudSpire\"), labels)\nfontsizes = fill(18.0, length(labels))\nfontsizes[outlier_idx] = 24.0\n\n# Jitter rotation and soften alpha for the dense Healthcare cluster, per the\n# spec's density-management guidance.\nrotations = zeros(Float64, length(labels))\nalphas = ones(Float64, length(labels))\nfor idx in healthcare_idx\n    rotations[idx] = deg2rad(rand(-12:12))\n    alphas[idx] = 0.82\nend\n\nlabel_colors = [RGBAf(c.r, c.g, c.b, a) for (c, a) in zip(point_colors, alphas)]\npoints = Point2f.(growth, margin)\n\n# Healthcare and Energy sit below WCAG 3:1 contrast on the cream bg as plain\n# text (no marker ink to fall back on) — give just those two an ink stroke.\nenergy_idx = findall(==(\"Energy\"), point_sectors)\nstroke_idx = vcat(healthcare_idx, energy_idx)\nplain_idx = findall(s -> s in (\"Technology\", \"Finance\"), point_sectors)\n\n# --- Plot -------------------------------------------------------------------\nfig = Figure(\n    resolution      = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title              = \"scatter-text · julia · makie · anyplot.ai\",\n    titlesize          = 20,\n    titlecolor         = INK,\n    xlabel             = \"Revenue Growth (%)\",\n    ylabel             = \"Profit Margin (%)\",\n    xlabelsize         = 14,\n    ylabelsize         = 14,\n    xlabelcolor        = INK,\n    ylabelcolor        = INK,\n    xticklabelsize     = 12,\n    yticklabelsize     = 12,\n    xticklabelcolor    = INK_SOFT,\n    yticklabelcolor    = INK_SOFT,\n    xtickcolor         = INK_SOFT,\n    ytickcolor         = INK_SOFT,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinecolor     = INK_SOFT,\n    bottomspinecolor   = INK_SOFT,\n    xgridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    ygridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    xminorgridvisible  = false,\n    yminorgridvisible  = false,\n)\n\ntext!(\n    ax, points[plain_idx];\n    text = labels[plain_idx],\n    color = label_colors[plain_idx],\n    fontsize = fontsizes[plain_idx],\n    rotation = rotations[plain_idx],\n    font = :bold,\n    align = (:center, :center),\n)\n\ntext!(\n    ax, points[stroke_idx];\n    text = labels[stroke_idx],\n    color = label_colors[stroke_idx],\n    fontsize = fontsizes[stroke_idx],\n    rotation = rotations[stroke_idx],\n    font = :bold,\n    align = (:center, :center),\n    strokewidth = 1.0,\n    strokecolor = INK,\n)\n\n# Callout for the growth outlier, reinforcing the emphasis from its larger fontsize.\ntext!(\n    ax, Point2f(growth[outlier_idx], margin[outlier_idx] - 2.6);\n    text = \"↑ fastest-growing\",\n    fontsize = 11,\n    font = :regular,\n    color = INK_SOFT,\n    align = (:center, :top),\n)\n\nxlims!(ax, -6, 42)\nylims!(ax, -2, 36)\n\nlegend_elements = [PolyElement(color = IMPRINT_PALETTE[i], strokecolor = :transparent) for i in 1:length(sectors)]\nLegend(\n    fig[1, 2], legend_elements, sectors, \"Sector\";\n    framevisible = false,\n    labelcolor = INK_SOFT,\n    titlecolor = INK,\n    backgroundcolor = PAGE_BG,\n)\ncolsize!(fig.layout, 1, Relative(0.85))\n\n# --- Save -------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}