{"spec_id":"line-load-duration","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nline-load-duration: Load Duration Curve for Energy Systems\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-10\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\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# Imprint palette — canonical positions 1→3 for three load regions\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nREGION_COLORS = {\n    \"Base load\": IMPRINT_PALETTE[0],  # #009E73 — steady, always-on\n    \"Intermediate load\": IMPRINT_PALETTE[1],  # #C475FD — cycling\n    \"Peak load\": IMPRINT_PALETTE[2],  # #4467A3 — brief demand spikes\n}\n\n# Seaborn theme with full theme-adaptive RC\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Data\nnp.random.seed(42)\nhours_in_year = 8760\nbase_load = 400\npeak_load_max = 1200\nt = np.linspace(0, 1, hours_in_year)\nload_profile = base_load + (peak_load_max - base_load) * (\n    0.5 * np.sin(2 * np.pi * t) ** 2\n    + 0.25 * np.sin(2 * np.pi * t * 365 / 7) ** 2\n    + 0.15 * np.random.normal(0, 1, hours_in_year)\n    + 0.1 * np.sin(2 * np.pi * t * 365) ** 2\n)\nload_profile = np.clip(load_profile, base_load * 0.9, peak_load_max * 1.05)\nload_sorted = np.sort(load_profile)[::-1]\nhours = np.arange(hours_in_year)\n\nbase_capacity = 500\nintermediate_capacity = 850\npeak_capacity = 1100\n\ntotal_energy_gwh = np.trapezoid(load_sorted, hours) / 1000\n\nregion = np.where(\n    load_sorted > intermediate_capacity,\n    \"Peak load\",\n    np.where(load_sorted > base_capacity, \"Intermediate load\", \"Base load\"),\n)\ndf = pd.DataFrame({\"hour\": hours, \"load_mw\": load_sorted, \"region\": region})\n\n# Plot\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400)\n\n# Filled regions under the load duration curve\nax.fill_between(\n    hours,\n    load_sorted,\n    intermediate_capacity,\n    where=(load_sorted > intermediate_capacity),\n    color=REGION_COLORS[\"Peak load\"],\n    alpha=0.25,\n)\nax.fill_between(\n    hours,\n    np.minimum(load_sorted, intermediate_capacity),\n    base_capacity,\n    where=(load_sorted > base_capacity),\n    color=REGION_COLORS[\"Intermediate load\"],\n    alpha=0.25,\n)\nax.fill_between(hours, np.minimum(load_sorted, base_capacity), 0, color=REGION_COLORS[\"Base load\"], alpha=0.25)\n\n# Load duration curve — seaborn hue mapping colors each segment by load region\n# hue_order starts with Base load so the legend's first entry is #009E73 (Imprint brand green)\nsns.lineplot(\n    data=df,\n    x=\"hour\",\n    y=\"load_mw\",\n    hue=\"region\",\n    hue_order=[\"Base load\", \"Intermediate load\", \"Peak load\"],\n    palette=REGION_COLORS,\n    linewidth=2.5,\n    legend=True,\n    ax=ax,\n)\n# Thin silhouette overlay to unify hue-segment transitions\nsns.lineplot(x=hours, y=load_sorted, color=INK, linewidth=0.8, alpha=0.3, legend=False, ax=ax)\n\n# Style the seaborn auto-legend via move_legend (removes default \"region\" title)\nsns.move_legend(ax, \"upper right\", fontsize=8, framealpha=0.92, title=\"\")\nax.get_legend().get_title().set_visible(False)\n\n# Capacity tier horizontal markers — labels at x=70% to separate from zone text labels\ncap_x = hours_in_year * 0.70\nfor capacity, label, color in [\n    (peak_capacity, \"Peak cap. (1,100 MW)\", REGION_COLORS[\"Peak load\"]),\n    (intermediate_capacity, \"Interm. cap. (850 MW)\", REGION_COLORS[\"Intermediate load\"]),\n    (base_capacity, \"Base cap. (500 MW)\", REGION_COLORS[\"Base load\"]),\n]:\n    ax.axhline(y=capacity, color=color, linestyle=\"--\", linewidth=1.2, alpha=0.7)\n    ax.text(cap_x, capacity + 18, label, fontsize=7, color=color, fontweight=\"semibold\")\n\n# Zone labels — centered within each load region\npeak_hours = int(np.sum(load_sorted > intermediate_capacity))\nbase_hours = int(np.sum(load_sorted > base_capacity))\n\nax.text(\n    peak_hours * 0.35,\n    (float(load_sorted[:peak_hours].mean()) + intermediate_capacity) / 2,\n    \"PEAK\",\n    fontsize=9,\n    fontweight=\"bold\",\n    color=REGION_COLORS[\"Peak load\"],\n    ha=\"center\",\n    va=\"center\",\n    alpha=0.85,\n)\nax.text(\n    (peak_hours + base_hours) / 2,\n    (intermediate_capacity + base_capacity) / 2,\n    \"INTERMEDIATE\",\n    fontsize=9,\n    fontweight=\"bold\",\n    color=REGION_COLORS[\"Intermediate load\"],\n    ha=\"center\",\n    va=\"center\",\n    alpha=0.85,\n)\nax.text(\n    (base_hours + hours_in_year) / 2,\n    base_capacity / 2,\n    \"BASE\",\n    fontsize=9,\n    fontweight=\"bold\",\n    color=REGION_COLORS[\"Base load\"],\n    ha=\"center\",\n    va=\"center\",\n    alpha=0.85,\n)\n\n# Total energy annotation — top-center of plot with Imprint-compliant callout box\nax.text(\n    hours_in_year * 0.28,\n    load_sorted.max() * 0.97,\n    f\"Total Energy: {total_energy_gwh:,.0f} GWh/year\",\n    fontsize=8,\n    fontweight=\"semibold\",\n    color=INK,\n    bbox={\n        \"boxstyle\": \"round,pad=0.4\",\n        \"facecolor\": ELEVATED_BG,\n        \"edgecolor\": INK_SOFT,\n        \"alpha\": 0.95,\n        \"linewidth\": 0.8,\n    },\n)\n\n# Style\nsns.despine(ax=ax)\nax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK)\nax.set_xlim(0, hours_in_year)\nax.set_ylim(0, load_sorted.max() * 1.08)\nax.set_title(\"line-load-duration · python · seaborn · anyplot.ai\", fontsize=12, fontweight=\"medium\", color=INK)\nax.set_xlabel(\"Hours of Year (ranked)\", fontsize=10, color=INK)\nax.set_ylabel(\"Power Demand (MW)\", fontsize=10, color=INK)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT)\n\n# Save — no bbox_inches so figsize × dpi lands exactly at 3200 × 1800 px\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}