{"spec_id":"range-interval","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nrange-interval: Range Interval Chart\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 99/100 | Updated: 2026-05-18\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport pandas as pd\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# Okabe-Ito palette\nBRAND = \"#009E73\"  # First series - always\nSECONDARY = \"#C475FD\"\nACCENT = \"#4467A3\"\n\n# Data: Monthly temperature ranges (°C) for a temperate city\ndata = pd.DataFrame(\n    {\n        \"month\": [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"],\n        \"min_temp\": [-2, -1, 3, 7, 12, 16, 18, 17, 13, 8, 3, 0],\n        \"max_temp\": [5, 7, 12, 18, 23, 27, 30, 29, 24, 17, 10, 6],\n    }\n)\n\n# Calculate midpoint for reference\ndata[\"mid_temp\"] = (data[\"min_temp\"] + data[\"max_temp\"]) / 2\n\n# Month order for proper sorting\nmonth_order = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\n# Create base chart\nbase = alt.Chart(data).encode(\n    y=alt.Y(\"month:N\", sort=month_order, title=\"Month\", axis=alt.Axis(labelFontSize=18, titleFontSize=22))\n)\n\n# Range bars (horizontal bars from min to max)\nbars = base.mark_bar(height=25, cornerRadius=4).encode(\n    x=alt.X(\"min_temp:Q\", title=\"Temperature (°C)\", axis=alt.Axis(labelFontSize=18, titleFontSize=22)),\n    x2=\"max_temp:Q\",\n    color=alt.value(BRAND),\n    opacity=alt.value(0.8),\n    tooltip=[\n        alt.Tooltip(\"month:N\", title=\"Month\"),\n        alt.Tooltip(\"min_temp:Q\", title=\"Min Temp (°C)\"),\n        alt.Tooltip(\"max_temp:Q\", title=\"Max Temp (°C)\"),\n        alt.Tooltip(\"mid_temp:Q\", title=\"Midpoint (°C)\", format=\".1f\"),\n    ],\n)\n\n# Min endpoint markers\nmin_points = base.mark_point(size=200, filled=True, shape=\"circle\").encode(x=\"min_temp:Q\", color=alt.value(SECONDARY))\n\n# Max endpoint markers\nmax_points = base.mark_point(size=200, filled=True, shape=\"circle\").encode(x=\"max_temp:Q\", color=alt.value(ACCENT))\n\n# Midpoint markers\nmid_points = base.mark_point(size=80, filled=True, shape=\"diamond\").encode(\n    x=\"mid_temp:Q\", color=alt.value(INK), stroke=alt.value(INK_SOFT), strokeWidth=alt.value(2)\n)\n\n# Layer all elements\nchart = (\n    alt.layer(bars, min_points, max_points, mid_points)\n    .properties(\n        width=1600,\n        height=900,\n        background=PAGE_BG,\n        title=alt.Title(\"range-interval · python · altair · anyplot.ai\", fontSize=28, anchor=\"middle\", color=INK),\n    )\n    .configure_axis(\n        labelFontSize=18,\n        titleFontSize=22,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridOpacity=0.10,\n        gridColor=INK,\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=1)\n    .configure_title(color=INK, fontSize=28)\n)\n\n# Save as PNG (scale_factor=3 for 4800x2700)\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\n\n# Save as HTML for interactivity\nchart.save(f\"plot-{THEME}.html\")\n"}