{"spec_id":"venn-labeled-items","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nvenn-labeled-items: Chartgeist-Style Venn Diagram with Labeled Items\nLibrary: letsplot 4.11.0 | Python 3.14.9\nQuality: 81/100 | Updated: 2026-10-31\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    coord_fixed,\n    element_rect,\n    geom_polygon,\n    geom_text,\n    ggplot,\n    ggsave,\n    ggsize,\n    theme,\n    theme_void,\n)\n\n\nLetsPlot.setup_html()\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette: all eight positions, one per circle in order\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Constants: the standard symmetric three-circle layout\nRADIUS = 1.5\nCENTERS = [(-0.85, 0.50), (0.85, 0.50), (0.00, -1.00)]  # circles A, B, C\nNAME_OFFSETS = [(-1.55, 1.45), (1.55, 1.45), (0.00, -1.85)]  # category name, from each circle's center\nNAME_HJUST = [1.0, 0.0, 0.5]  # aligned away from the diagram's center\nOUTSIDE = \"outside\"\nMARGIN_X = 1.95  # empty space left and right of the circles\nNAME_ZONE = \"name \"  # zone prefix of a row that holds a circle's category name\n\n# Columns: set these to your table's column names\nLABEL = \"label\"  # role: label\nZONE = \"zone\"  # role: zone\nX = \"x\"  # role: extra\nY = \"y\"  # role: extra\n\n# Data: replace this block with your own table, for example df = pd.read_csv(\"items.csv\")\n# Zone codes: A, B, C, AB, AC, BC, ABC or outside; x and y are the label's place inside its zone\ndf = pd.DataFrame(\n    [\n        (\"Overhyped\", NAME_ZONE + \"A\", np.nan, np.nan),\n        (\"Actually Useful\", NAME_ZONE + \"B\", np.nan, np.nan),\n        (\"Secretly Loved\", NAME_ZONE + \"C\", np.nan, np.nan),\n        (\"NFTs\", \"A\", -1.75, 1.05),\n        (\"Metaverse\", \"A\", -1.80, 0.55),\n        (\"Smart Fridges\", \"A\", -1.70, 0.05),\n        (\"Google Maps\", \"B\", 1.75, 1.05),\n        (\"Spreadsheets\", \"B\", 1.80, 0.55),\n        (\"Calendar Apps\", \"B\", 1.70, 0.05),\n        (\"Roller Skating\", \"C\", -0.75, -2.05),\n        (\"Soap Operas\", \"C\", 0.75, -2.05),\n        (\"ChatGPT\", \"AB\", 0.00, 1.20),\n        (\"Smartwatches\", \"AB\", 0.00, 0.85),\n        (\"Crocs\", \"AC\", -1.05, -0.20),\n        (\"Vinyl Records\", \"AC\", -1.00, -0.60),\n        (\"Dolly Parton\", \"BC\", 1.05, -0.20),\n        (\"Spotify\", \"BC\", 1.00, -0.60),\n        (\"Sourdough\", \"ABC\", 0.00, 0.15),\n        (\"TikTok\", \"ABC\", 0.00, -0.30),\n        (\"Beige Walls\", OUTSIDE, -3.10, -2.50),\n    ],\n    columns=[LABEL, ZONE, X, Y],\n)\n\n# Plot: reads only df and the constants above\ntheta = np.linspace(0, 2 * np.pi, 240)\ncenters = np.array(CENTERS)\nhalf = centers[:, 0].max() + RADIUS + MARGIN_X\nmid_y = (centers[:, 1].min() - RADIUS + centers[:, 1].max() + RADIUS) / 2\nxlim = [-half, half]\nylim = [mid_y - half, mid_y + half]  # square panel: fills the square canvas\n\nis_name = df[ZONE].str.startswith(NAME_ZONE)\nnames = df[is_name].drop_duplicates(ZONE)\nitems = df[~is_name]\n\nplot = ggplot() + coord_fixed(xlim=xlim, ylim=ylim)\n\n# Three overlapping circles with semi-transparent fills\nfor i, (cx, cy) in enumerate(CENTERS):\n    circle_df = pd.DataFrame({X: cx + RADIUS * np.cos(theta), Y: cy + RADIUS * np.sin(theta)})\n    plot += geom_polygon(\n        aes(x=X, y=Y), data=circle_df, fill=IMPRINT_PALETTE[i], color=IMPRINT_PALETTE[i], alpha=0.22, size=0.7\n    )\n\n# Item labels (serif, INK)\nplot += geom_text(aes(x=X, y=Y, label=LABEL), data=items, size=4.5, family=\"serif\", color=INK)\n\n# Category names outside each circle, in the circle's color\nfor i, name in enumerate(names[LABEL]):\n    dx, dy = NAME_OFFSETS[i]\n    plot += geom_text(\n        x=CENTERS[i][0] + dx,\n        y=CENTERS[i][1] + dy,\n        label=name,\n        size=7,\n        family=\"serif\",\n        fontface=\"bold\",\n        color=IMPRINT_PALETTE[i],\n        hjust=NAME_HJUST[i],\n    )\n\n# Hint above the outside items\noutside = items[items[ZONE] == OUTSIDE]\nif len(outside):\n    plot += geom_text(\n        x=outside[X].mean(),\n        y=outside[Y].max() + 0.30,\n        label=\"(neither here nor there)\",\n        size=3.5,\n        family=\"serif\",\n        fontface=\"italic\",\n        color=INK_MUTED,\n        hjust=0.5,\n    )\n\n# Editorial kicker + canonical anyplot.ai title line\nmid = sum(xlim) / 2\nplot += geom_text(\n    x=mid, y=ylim[1] - 0.30, label=\"Chartgeist 2026\", size=9, family=\"serif\", fontface=\"bold_italic\", color=INK\n)\nplot += geom_text(\n    x=mid, y=ylim[1] - 0.80, label=\"venn-labeled-items · letsplot · anyplot.ai\", size=4, family=\"serif\", color=INK_MUTED\n)\n\n# Style: gridless editorial background\nplot += theme_void() + theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    legend_position=\"none\",\n)\nplot += ggsize(600, 600)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}