{"spec_id":"violin-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nviolin-basic: Basic Violin Plot\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-29\n\"\"\"\n\nimport importlib\nimport os\nimport sys\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Drop script directory from sys.path so `altair` resolves the package, not this file\nsys.path[:] = [p for p in sys.path if os.path.abspath(p or \".\") != os.path.dirname(os.path.abspath(__file__))]\nalt = importlib.import_module(\"altair\")\n\n# Theme tokens — Imprint palette + theme-adaptive chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Imprint categorical palette — positions 1-4 for four departments\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data — salary distributions by department with distinct shapes\nnp.random.seed(42)\ndept_records = []\nfor dept, mu, sigma, n in [\n    (\"Support\", 55000, 10000, 150),\n    (\"Marketing\", 70000, 13000, 150),\n    (\"Engineering\", 92000, 16000, 150),\n]:\n    for v in np.random.normal(mu, sigma, n):\n        dept_records.append({\"Department\": dept, \"Salary\": v})\n\n# Sales: bimodal — base salary + commission earners (highlights violin over box plot)\nfor v in np.concatenate([np.random.normal(50000, 8000, 75), np.random.normal(92000, 11000, 75)]):\n    dept_records.append({\"Department\": \"Sales\", \"Salary\": v})\n\ndf = pd.DataFrame(dept_records)\ndept_order = [\"Support\", \"Marketing\", \"Engineering\", \"Sales\"]\ncolor_scale = alt.Scale(domain=dept_order, range=IMPRINT_PALETTE)\n\nbase = alt.Chart(df)\n\n# Violin shape — KDE, mirrored via stack=\"center\"\nviolin = (\n    base.transform_density(\n        density=\"Salary\",\n        as_=[\"Salary\", \"density\"],\n        groupby=[\"Department\"],\n        extent=[int(df[\"Salary\"].min()) - 8000, int(df[\"Salary\"].max()) + 8000],\n    )\n    .mark_area(orient=\"horizontal\", opacity=0.75)\n    .encode(\n        y=alt.Y(\"Salary:Q\", title=\"Salary ($)\"),\n        x=alt.X(\n            \"density:Q\",\n            stack=\"center\",\n            impute=None,\n            title=None,\n            axis=alt.Axis(labels=False, values=[0], grid=False, ticks=False),\n        ),\n        color=alt.Color(\"Department:N\", scale=color_scale, legend=None),\n        tooltip=[alt.Tooltip(\"Department:N\"), alt.Tooltip(\"Salary:Q\", format=\"$,.0f\")],\n    )\n)\n\n# IQR marker — thick vertical rule from Q1 to Q3\nquartile_rule = (\n    base.transform_aggregate(q1=\"q1(Salary)\", q3=\"q3(Salary)\", groupby=[\"Department\"])\n    .mark_rule(color=INK, strokeWidth=4)\n    .encode(y=\"q1:Q\", y2=\"q3:Q\")\n)\n\n# Median line — horizontal rule at the median, contrasts against the IQR bar\nmedian_line = (\n    base.transform_aggregate(med=\"median(Salary)\", groupby=[\"Department\"])\n    .mark_rule(color=PAGE_BG, strokeWidth=3)\n    .encode(\n        y=alt.Y(\"med:Q\"),\n        tooltip=[alt.Tooltip(\"Department:N\"), alt.Tooltip(\"med:Q\", title=\"Median Salary\", format=\"$,.0f\")],\n    )\n)\n\n# Title — 43 chars, below 67-char threshold so fontSize=16 is fine\ntitle_str = \"violin-basic · python · altair · anyplot.ai\"\nn_chars = len(title_str)\nratio = 67 / n_chars if n_chars > 67 else 1.0\ntitle_fontsize = max(11, round(16 * ratio))\n\n# Chart — four violin panels faceted side-by-side; dept ordering tells salary story\nchart = (\n    alt.layer(violin, quartile_rule, median_line)\n    .facet(\n        column=alt.Column(\n            \"Department:N\",\n            header=alt.Header(labelFontSize=16, labelOrient=\"bottom\", title=None, labelPadding=12, labelColor=INK_SOFT),\n            sort=dept_order,\n        )\n    )\n    .resolve_scale(x=\"independent\")\n    .properties(background=PAGE_BG, title=alt.Title(title_str, fontSize=title_fontsize, anchor=\"middle\", color=INK))\n    .configure_facet(spacing=15)\n    .configure_view(stroke=None, fill=PAGE_BG, continuousWidth=160, continuousHeight=340)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        gridColor=INK,\n        gridOpacity=0.12,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n    )\n    .configure_title(color=INK)\n)\n\n# Save PNG + pad to exact 3200 × 1800 target (vl-convert pads title/axis outside view dims)\nTW, TH = 3200, 1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        \"Shrink continuousWidth/continuousHeight and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save HTML\nchart.save(f\"plot-{THEME}.html\")\n"}