Chris Parmer — home

Ragone plot

Energy Storage — specific energy vs. specific power, log-log

Example from the compendium of canonical charts

Energy Storage — Ragone plot (specific energy vs. specific power, log-log)

Python Code

"""Energy Storage — Ragone plot (specific energy vs. specific power, log-log)."""
from pathlib import Path

# ── Palette + theme (matching the Plotly Studio gallery these charts ship in) ──
VIOLET, TEAL, GREEN, PINK, ORANGE = "#845EEE", "#52B3D0", "#55B685", "#DA5597", "#E9A23B"
PRIMARY, SECONDARY = VIOLET, TEAL
COLORWAY = [VIOLET, TEAL, GREEN, PINK, ORANGE]
BG, TEXT, GRID, MUTED = "#ffffff", "#1c2024", "#d9d9e0", "#60646c"
FONT = "Inter, -apple-system, BlinkMacSystemFont, sans-serif"
COLORSCALE = [[0, "rgba(132, 94, 238, 0.05)"], [1, "rgba(132, 94, 238, 0.9)"]]


def apply_theme(fig):
    """Light gallery theme: white background, Inter font, soft gridlines."""
    fig.update_layout(
        paper_bgcolor=BG, plot_bgcolor=BG, colorway=COLORWAY,
        font=dict(family=FONT, color=TEXT, size=12),
        legend=dict(font=dict(color=TEXT)),
        hoverlabel=dict(bgcolor="#f0f0f3", font=dict(color=TEXT, family=FONT), bordercolor=GRID),
    )
    fig.update_xaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)
    fig.update_yaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)


def fetch_csv(url, **kwargs):
    import io
    import pandas as pd
    import requests
    r = requests.get(url, timeout=60)
    r.raise_for_status()
    return pd.read_csv(io.StringIO(r.text), **kwargs)


def fetch_json(url):
    import requests
    r = requests.get(url, timeout=60)
    r.raise_for_status()
    return r.json()

import numpy as np
import plotly.graph_objects as go


TECHNOLOGIES = {
    "Supercapacitor": {
        "energy_range": [2, 10],
        "power_range": [1000, 10000],
        "color": TEAL,
    },
    "Li-ion": {
        "energy_range": [100, 250],
        "power_range": [200, 2000],
        "color": VIOLET,
    },
    "NiMH": {
        "energy_range": [60, 120],
        "power_range": [150, 1000],
        "color": GREEN,
    },
    "Lead-acid": {
        "energy_range": [30, 50],
        "power_range": [100, 400],
        "color": ORANGE,
    },
    "Fuel cell": {
        "energy_range": [300, 3000],
        "power_range": [10, 100],
        "color": PINK,
    },
}


def hex_to_rgba(hex_str, alpha=0.25):
    """Convert #RRGGBB to rgba(r,g,b,a) string."""
    h = hex_str.lstrip("#")
    r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
    return f"rgba({r},{g},{b},{alpha})"


def make_ellipse_path(xmin, xmax, ymin, ymax, n=60):
    """Return (x, y) arrays for an ellipse inscribed in the log-space bounding box."""
    lxc = (np.log10(xmin) + np.log10(xmax)) / 2
    lyc = (np.log10(ymin) + np.log10(ymax)) / 2
    lxa = (np.log10(xmax) - np.log10(xmin)) / 2
    lya = (np.log10(ymax) - np.log10(ymin)) / 2
    theta = np.linspace(0, 2 * np.pi, n + 1)
    lx = lxc + lxa * np.cos(theta)
    ly = lyc + lya * np.sin(theta)
    return 10 ** lx, 10 ** ly


def generate():
    fig = go.Figure()

    # ── Constant discharge-time lines (diagonal guides) ──────────────────────
    E_line = np.logspace(0, 4, 200)
    for t_h, label in [(1.0, "1 h"), (0.1, "6 min"), (0.01, "36 s")]:
        P_line = E_line / t_h
        fig.add_trace(go.Scatter(
            x=E_line,
            y=P_line,
            mode="lines",
            line=dict(color="#b0b0b0", width=1, dash="dot"),
            showlegend=False,
            hoverinfo="skip",
        ))

    # ── Technology ellipses ───────────────────────────────────────────────────
    for tech, props in TECHNOLOGIES.items():
        e_min, e_max = props["energy_range"]
        p_min, p_max = props["power_range"]
        color = props["color"]

        xe, ye = make_ellipse_path(e_min, e_max, p_min, p_max)
        fig.add_trace(go.Scatter(
            x=xe,
            y=ye,
            mode="lines",
            fill="toself",
            fillcolor=hex_to_rgba(color, 0.25),
            line=dict(color=color, width=2),
            name=tech,
            hovertemplate=(
                f"<b>{tech}</b><br>"
                f"Energy: {e_min}–{e_max} Wh/kg<br>"
                f"Power: {p_min}–{p_max} W/kg<extra></extra>"
            ),
        ))

        # Label at centroid of ellipse (in log space)
        xc = np.sqrt(e_min * e_max)
        yc = np.sqrt(p_min * p_max)
        fig.add_annotation(
            x=np.log10(xc),
            y=np.log10(yc),
            text=f"<b>{tech}</b>",
            showarrow=False,
            font=dict(size=10, color=color),
            xref="x",
            yref="y",
        )

    # ── Discharge-time axis labels ────────────────────────────────────────────
    for t_h, label in [(1.0, "1 h"), (0.1, "6 min"), (0.01, "36 s")]:
        x_label = 2e3
        y_label = x_label / t_h
        if 5 < y_label < 5e4:
            fig.add_annotation(
                x=np.log10(x_label),
                y=np.log10(y_label),
                text=label,
                showarrow=False,
                font=dict(size=12, color="#555555"),
                xref="x",
                yref="y",
                textangle=-35,
            )

    fig.update_layout(
        xaxis=dict(
            title="Specific Energy (Wh/kg)",
            type="log",
            range=[0, 4],
            showgrid=True,
        ),
        yaxis=dict(
            title="Specific Power (W/kg)",
            type="log",
            range=[0.5, 4.5],
            showgrid=True,
        ),
        legend=dict(
            x=1.02, y=1.0,
            xanchor="left",
            font=dict(size=10),
        ),
        margin=dict(t=20, b=60, l=70, r=140),
    )

    apply_theme(fig)
    return fig


fig = generate()
fig.show()

Made with Plotly