{"spec_id":"facet-grid","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nfacet-grid: Faceted Grid Plot\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 85/100 | Updated: 2026-05-13\n\"\"\"\n\nimport os\nfrom io import BytesIO\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom PIL import Image, ImageDraw, ImageFont\nfrom pygal.style import Style\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\n# Theme-adaptive colors\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# Okabe-Ito palette (first series = brand green)\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\")\n\n# Data - Plant growth experiment across different soil types and light conditions\nnp.random.seed(42)\n\nrow_cats = [\"Sandy Soil\", \"Clay Soil\"]\ncol_cats = [\"Low Light\", \"Medium Light\", \"High Light\"]\n\n# Generate plant growth data (height vs days) for each condition\ndata = {}\nbase_growth_rates = {\n    (\"Sandy Soil\", \"Low Light\"): 0.4,\n    (\"Sandy Soil\", \"Medium Light\"): 0.8,\n    (\"Sandy Soil\", \"High Light\"): 1.0,\n    (\"Clay Soil\", \"Low Light\"): 0.5,\n    (\"Clay Soil\", \"Medium Light\"): 1.2,\n    (\"Clay Soil\", \"High Light\"): 0.9,\n}\n\ndays = [0, 5, 10, 15, 20, 25, 30]\n\nfor row_cat in row_cats:\n    for col_cat in col_cats:\n        rate = base_growth_rates[(row_cat, col_cat)]\n        heights = [d * rate + np.random.normal(0, 1) for d in days]\n        heights = [max(0, h) for h in heights]\n        data[(row_cat, col_cat)] = heights\n\n# Custom style for theme-adaptive rendering\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT,\n    font_family=\"sans-serif\",\n    title_font_size=28,\n    label_font_size=18,\n    major_label_font_size=16,\n    legend_font_size=16,\n    value_font_size=14,\n    stroke_width=3,\n)\n\n# Create individual charts for each facet\ncell_width = 1500\ncell_height = 1250\n\ncharts = []\nfor row_idx, row_cat in enumerate(row_cats):\n    row_charts = []\n    for col_idx, col_cat in enumerate(col_cats):\n        chart = pygal.Line(\n            width=cell_width,\n            height=cell_height,\n            style=custom_style,\n            show_legend=False,\n            show_y_guides=True,\n            show_x_guides=False,\n            x_title=\"Days\" if row_idx == len(row_cats) - 1 else \"\",\n            y_title=\"Height (cm)\" if col_idx == 0 else \"\",\n            title=f\"{col_cat}\" if row_idx == 0 else \"\",\n            show_dots=True,\n            dots_size=8,\n            stroke_style={\"width\": 4},\n            range=(0, 35),\n            truncate_label=-1,\n        )\n\n        chart.x_labels = [str(d) for d in days]\n        chart.add(row_cat, data[(row_cat, col_cat)])\n        row_charts.append(chart)\n    charts.append(row_charts)\n\n# Render each chart to PNG and combine them\nimages = []\nfor row_charts in charts:\n    row_images = []\n    for chart in row_charts:\n        svg_bytes = chart.render()\n        png_bytes = cairosvg.svg2png(bytestring=svg_bytes, output_width=cell_width, output_height=cell_height)\n        img = Image.open(BytesIO(png_bytes))\n        row_images.append(img)\n    images.append(row_images)\n\n# Create combined image grid\ntitle_height = 150\nrow_label_width = 300\ntotal_width = 4800\ntotal_height = 2700\n\ncombined = Image.new(\"RGB\", (total_width, total_height), PAGE_BG)\n\n# Calculate grid positioning\ngrid_width = total_width - row_label_width\ngrid_height = total_height - title_height\nactual_cell_width = grid_width // len(col_cats)\nactual_cell_height = grid_height // len(row_cats)\n\n# Paste charts into grid\nfor row_idx, row_images in enumerate(images):\n    for col_idx, img in enumerate(row_images):\n        img_resized = img.resize((actual_cell_width, actual_cell_height), Image.LANCZOS)\n        x = row_label_width + col_idx * actual_cell_width\n        y = title_height + row_idx * actual_cell_height\n        combined.paste(img_resized, (x, y))\n\n# Add text annotations using PIL\ndraw = ImageDraw.Draw(combined)\n\ntry:\n    title_font = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf\", 60)\n    label_font = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf\", 42)\nexcept OSError:\n    title_font = ImageFont.load_default()\n    label_font = ImageFont.load_default()\n\n# Draw main title\ntitle_text = \"facet-grid · pygal · anyplot.ai\"\nbbox = draw.textbbox((0, 0), title_text, font=title_font)\ntitle_width = bbox[2] - bbox[0]\ntitle_x = (total_width - title_width) // 2\ndraw.text((title_x, 40), title_text, fill=INK, font=title_font)\n\n# Draw row labels (rotated 90 degrees)\nfor row_idx, row_cat in enumerate(row_cats):\n    temp_img = Image.new(\"RGBA\", (450, 100), (255, 255, 255, 0))\n    temp_draw = ImageDraw.Draw(temp_img)\n    temp_draw.text((10, 10), row_cat, fill=INK, font=label_font)\n    temp_img = temp_img.rotate(90, expand=True)\n\n    label_y = title_height + row_idx * actual_cell_height + actual_cell_height // 2 - temp_img.height // 2\n    combined.paste(temp_img, (40, label_y), temp_img)\n\n# Save output files with theme-suffixed names\ncombined.save(f\"plot-{THEME}.png\", dpi=(300, 300))\n\n# Also save as interactive HTML\nhtml_content = (\n    \"\"\"<!DOCTYPE html>\n<html>\n<head>\n    <title>facet-grid · pygal · anyplot.ai</title>\n    <style>\n        body { font-family: sans-serif; background: \"\"\"\n    + PAGE_BG\n    + \"\"\"; margin: 20px; }\n        h1 { text-align: center; color: \"\"\"\n    + INK\n    + \"\"\"; font-size: 28px; }\n        .grid { display: grid; grid-template-columns: 100px repeat(3, 1fr); gap: 10px; max-width: 1600px; margin: 0 auto; }\n        .row-label { writing-mode: vertical-rl; text-orientation: mixed; transform: rotate(180deg);\n                     display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: bold; color: \"\"\"\n    + INK\n    + \"\"\"; }\n        .col-header { text-align: center; font-size: 20px; font-weight: bold; color: \"\"\"\n    + INK\n    + \"\"\"; padding: 10px; }\n        .chart { width: 100%; }\n        .empty { }\n    </style>\n</head>\n<body>\n    <h1>facet-grid · pygal · anyplot.ai</h1>\n    <div class=\"grid\">\n        <div class=\"empty\"></div>\n\"\"\"\n)\n\n# Add column headers\nfor col_cat in col_cats:\n    html_content += f'        <div class=\"col-header\">{col_cat}</div>\\n'\n\n# Add charts with row labels\nfor row_idx, row_cat in enumerate(row_cats):\n    html_content += f'        <div class=\"row-label\">{row_cat}</div>\\n'\n    for col_idx, _col_cat in enumerate(col_cats):\n        chart = charts[row_idx][col_idx]\n        svg_data = chart.render(is_unicode=True)\n        svg_data = svg_data.replace('<?xml version=\"1.0\" encoding=\"utf-8\"?>', \"\")\n        html_content += f'        <div class=\"chart\">{svg_data}</div>\\n'\n\nhtml_content += \"\"\"    </div>\n</body>\n</html>\"\"\"\n\nwith open(f\"plot-{THEME}.html\", \"w\") as f:\n    f.write(html_content)\n"}