{"spec_id":"dashboard-metrics-tiles","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ndashboard-metrics-tiles: Real-Time Dashboard Tiles\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-21\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove the script's own directory from sys.path to prevent circular import\n# (this file is named pygal.py, same as the installed package).\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p or \".\") != _script_dir]\n\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\n# Theme tokens\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# Semantic status colors (Okabe-Ito positions)\nSTATUS_COLORS = {\"good\": \"#009E73\", \"warning\": \"#DDCC77\", \"critical\": \"#AE3030\"}  # imprint semantic anchors\nCHANGE_POSITIVE = \"#009E73\"\nCHANGE_NEGATIVE = \"#AE3030\"  # imprint red — negative change\nSPARKLINE_COLOR = \"#4467A3\"  # Okabe-Ito position 3\n\n# Data\nnp.random.seed(42)\nmetrics = [\n    {\n        \"name\": \"CPU Usage\",\n        \"value\": 45,\n        \"unit\": \"%\",\n        \"change\": -5.2,\n        \"status\": \"good\",\n        \"history\": [52, 48, 55, 51, 47, 50, 48, 45, 46, 44, 45],\n        \"lower_is_better\": True,\n    },\n    {\n        \"name\": \"Memory\",\n        \"value\": 72,\n        \"unit\": \"%\",\n        \"change\": 8.3,\n        \"status\": \"warning\",\n        \"history\": [65, 66, 68, 67, 70, 69, 71, 70, 72, 71, 72],\n        \"lower_is_better\": True,\n    },\n    {\n        \"name\": \"Response Time\",\n        \"value\": 120,\n        \"unit\": \"ms\",\n        \"change\": -15.0,\n        \"status\": \"good\",\n        \"history\": [145, 142, 138, 135, 130, 128, 125, 122, 121, 120, 120],\n        \"lower_is_better\": True,\n    },\n    {\n        \"name\": \"Error Rate\",\n        \"value\": 2.1,\n        \"unit\": \"%\",\n        \"change\": 45.0,\n        \"status\": \"critical\",\n        \"history\": [1.2, 1.4, 1.3, 1.5, 1.6, 1.8, 1.9, 2.0, 2.0, 2.1, 2.1],\n        \"lower_is_better\": True,\n    },\n    {\n        \"name\": \"Throughput\",\n        \"value\": 1250,\n        \"unit\": \"req/s\",\n        \"change\": 12.5,\n        \"status\": \"good\",\n        \"history\": [1100, 1120, 1150, 1180, 1200, 1210, 1220, 1230, 1240, 1245, 1250],\n        \"lower_is_better\": False,\n    },\n    {\n        \"name\": \"Active Users\",\n        \"value\": 3420,\n        \"unit\": \"\",\n        \"change\": 5.8,\n        \"status\": \"good\",\n        \"history\": [3200, 3220, 3280, 3310, 3350, 3380, 3390, 3400, 3410, 3415, 3420],\n        \"lower_is_better\": False,\n    },\n]\n\n# Canvas layout (3200x1800 landscape — hard contract)\nCANVAS_WIDTH = 3200\nCANVAS_HEIGHT = 1800\nTITLE_HEIGHT = 120\nMARGIN = 30\nGAP = 30\nGRID_COLS = 3\nGRID_ROWS = 2\n\ngrid_width = CANVAS_WIDTH - 2 * MARGIN\ngrid_height = CANVAS_HEIGHT - TITLE_HEIGHT - 2 * MARGIN\ntile_width = (grid_width - (GRID_COLS - 1) * GAP) // GRID_COLS\ntile_height = (grid_height - (GRID_ROWS - 1) * GAP) // GRID_ROWS\n\nTILE_PADDING = 40\nSTATUS_BAR_H = 10\nSPARKLINE_W = tile_width - 2 * TILE_PADDING\nSPARKLINE_H = 340\n\n# Pygal sparkline style (transparent background)\nsparkline_style = Style(\n    background=\"transparent\",\n    plot_background=\"transparent\",\n    foreground=\"transparent\",\n    foreground_strong=\"transparent\",\n    foreground_subtle=\"transparent\",\n    colors=(SPARKLINE_COLOR,),\n    stroke_width=5,\n)\n\n# Load fonts\ntry:\n    title_font = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf\", 68)\n    value_font = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf\", 100)\n    name_font = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf\", 46)\n    change_font = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf\", 52)\n    trend_font = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf\", 36)\nexcept OSError:\n    title_font = ImageFont.load_default()\n    value_font = ImageFont.load_default()\n    name_font = ImageFont.load_default()\n    change_font = ImageFont.load_default()\n    trend_font = ImageFont.load_default()\n\n# Create canvas\ncanvas = Image.new(\"RGB\", (CANVAS_WIDTH, CANVAS_HEIGHT), PAGE_BG)\ndraw = ImageDraw.Draw(canvas)\n\n# Title\ntitle_text = \"dashboard-metrics-tiles · python · pygal · anyplot.ai\"\nbbox = draw.textbbox((0, 0), title_text, font=title_font)\ntitle_w = bbox[2] - bbox[0]\ndraw.text(((CANVAS_WIDTH - title_w) // 2, 32), title_text, fill=INK, font=title_font)\n\n# Metric tiles — 3x2 grid\nfor idx, metric in enumerate(metrics):\n    row = idx // GRID_COLS\n    col = idx % GRID_COLS\n    tx = MARGIN + col * (tile_width + GAP)\n    ty = TITLE_HEIGHT + MARGIN + row * (tile_height + GAP)\n\n    # Tile background\n    draw.rounded_rectangle([tx, ty, tx + tile_width, ty + tile_height], radius=18, fill=ELEVATED_BG)\n\n    # Coloured status bar at top of tile\n    draw.rectangle([tx, ty, tx + tile_width, ty + STATUS_BAR_H], fill=STATUS_COLORS[metric[\"status\"]])\n\n    # Metric name\n    cy = ty + STATUS_BAR_H + TILE_PADDING\n    draw.text((tx + TILE_PADDING, cy), metric[\"name\"], fill=INK_SOFT, font=name_font)\n    cy += 65\n\n    # Current value with unit\n    value_text = f\"{metric['value']:,}{metric['unit']}\"\n    draw.text((tx + TILE_PADDING, cy), value_text, fill=INK, font=value_font)\n    cy += 130\n\n    # Change indicator (arrow + percentage)\n    change = metric[\"change\"]\n    favorable = (change < 0) if metric[\"lower_is_better\"] else (change > 0)\n    change_color = CHANGE_POSITIVE if favorable else CHANGE_NEGATIVE\n    arrow = \"▲\" if change > 0 else \"▼\"\n    draw.text((tx + TILE_PADDING, cy), f\"{arrow} {abs(change):.1f}%\", fill=change_color, font=change_font)\n\n    # Separator + trend label above sparkline to fill the vertical gap\n    sp_y = ty + tile_height - SPARKLINE_H - TILE_PADDING\n    rule_y = sp_y - 52\n    draw.line([(tx + TILE_PADDING, rule_y), (tx + tile_width - TILE_PADDING, rule_y)], fill=INK_SOFT, width=2)\n    draw.text((tx + TILE_PADDING, rule_y + 10), \"7-day trend\", fill=INK_SOFT, font=trend_font)\n\n    # Sparkline (full tile width, anchored to bottom of tile)\n    sp_chart = pygal.Line(\n        width=SPARKLINE_W,\n        height=SPARKLINE_H,\n        style=sparkline_style,\n        show_legend=False,\n        show_dots=False,\n        show_y_labels=False,\n        show_x_labels=False,\n        show_y_guides=False,\n        show_x_guides=False,\n        margin=0,\n        spacing=0,\n        fill=True,\n        stroke_style={\"width\": 5, \"linecap\": \"round\", \"linejoin\": \"round\"},\n    )\n    sp_chart.add(\"\", metric[\"history\"])\n    sp_svg = sp_chart.render()\n    sp_png = cairosvg.svg2png(bytestring=sp_svg, output_width=SPARKLINE_W, output_height=SPARKLINE_H)\n    sp_img = Image.open(BytesIO(sp_png)).convert(\"RGBA\")\n    canvas.paste(sp_img, (tx + TILE_PADDING, sp_y), sp_img)\n\n# Save PNG\ncanvas.save(f\"plot-{THEME}.png\")\n\n# Interactive HTML with pygal SVG sparklines\nhtml_parts = [\n    f\"\"\"<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <title>dashboard-metrics-tiles · python · pygal · anyplot.ai</title>\n    <style>\n        * {{ box-sizing: border-box; margin: 0; padding: 0; }}\n        body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n               background: {PAGE_BG}; padding: 40px; }}\n        h1 {{ text-align: center; color: {INK}; font-size: 26px; margin-bottom: 32px; }}\n        .dashboard {{ display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;\n                     max-width: 1400px; margin: 0 auto; }}\n        .tile {{ background: {ELEVATED_BG}; border-radius: 12px; padding: 24px;\n                position: relative; overflow: hidden; }}\n        .status-bar {{ position: absolute; top: 0; left: 0; right: 0; height: 4px; }}\n        .metric-name {{ color: {INK_SOFT}; font-size: 15px; margin-bottom: 8px; }}\n        .metric-value {{ color: {INK}; font-size: 38px; font-weight: bold; margin-bottom: 8px; }}\n        .metric-change {{ font-size: 16px; font-weight: 600; margin-bottom: 12px; }}\n        .change-pos {{ color: #009E73; }}\n        .change-neg {{ color: #C475FD; }}\n        .trend-separator {{ border: none; border-top: 1px solid {INK_SOFT}; margin: 0 0 6px; }}\n        .trend-label {{ font-size: 12px; color: {INK_SOFT}; margin-bottom: 6px; }}\n        .sparkline {{ width: 100%; height: 70px; overflow: hidden; }}\n        .sparkline svg {{ width: 100%; height: 100%; }}\n        @media (max-width: 900px) {{ .dashboard {{ grid-template-columns: repeat(2, 1fr); }} }}\n        @media (max-width: 600px) {{ .dashboard {{ grid-template-columns: 1fr; }} }}\n    </style>\n</head>\n<body>\n    <h1>dashboard-metrics-tiles · python · pygal · anyplot.ai</h1>\n    <div class=\"dashboard\">\n\"\"\"\n]\n\nfor metric in metrics:\n    change = metric[\"change\"]\n    favorable = (change < 0) if metric[\"lower_is_better\"] else (change > 0)\n    change_class = \"change-pos\" if favorable else \"change-neg\"\n    arrow = \"▲\" if change > 0 else \"▼\"\n\n    mini = pygal.Line(\n        width=300,\n        height=80,\n        style=sparkline_style,\n        show_legend=False,\n        show_dots=False,\n        show_y_labels=False,\n        show_x_labels=False,\n        show_y_guides=False,\n        show_x_guides=False,\n        margin=2,\n        fill=True,\n    )\n    mini.add(\"\", metric[\"history\"])\n    sp_svg = mini.render(is_unicode=True).replace('<?xml version=\"1.0\" encoding=\"utf-8\"?>', \"\")\n\n    html_parts.append(\n        f\"\"\"        <div class=\"tile\">\n            <div class=\"status-bar\" style=\"background:{STATUS_COLORS[metric[\"status\"]]}\"></div>\n            <div class=\"metric-name\">{metric[\"name\"]}</div>\n            <div class=\"metric-value\">{metric[\"value\"]:,}{metric[\"unit\"]}</div>\n            <div class=\"metric-change {change_class}\">{arrow} {abs(change):.1f}%</div>\n            <hr class=\"trend-separator\">\n            <div class=\"trend-label\">7-day trend</div>\n            <div class=\"sparkline\">{sp_svg}</div>\n        </div>\n\"\"\"\n    )\n\nhtml_parts.append(\n    \"\"\"    </div>\n</body>\n</html>\"\"\"\n)\n\nwith open(f\"plot-{THEME}.html\", \"w\") as f:\n    f.write(\"\".join(html_parts))\n"}