Plotly Accessible Plot Examples

Code examples showing how to make Plotly bar charts, histograms, line charts, box plots, heatmaps, scatter plots, precision-recall curves and percentile bands accessible with py-maidr.

Plotly Examples

maidr also supports Plotly figures. If you already have data visualization code using plotly.graph_objects, you can make your plots accessible with maidr in just a few lines of code. For matplotlib and seaborn examples, see the Matplotlib / Seaborn Examples page.

Simply import the maidr package and pass your Plotly Figure to maidr.show(). maidr will automatically generate an accessible HTML version with sonification, braille, and keyboard navigation support.

Bar Plot

Show the code
import plotly.graph_objects as go
import seaborn as sns

import maidr  

# Load the penguins dataset
penguins = sns.load_dataset("penguins")

# Calculate average body mass by species
avg_mass = penguins.groupby("species")["body_mass_g"].mean()

# Create a bar plot showing the average body mass of penguins by species
fig = go.Figure(
    data=[
        go.Bar(
            x=avg_mass.index.tolist(),
            y=avg_mass.values.tolist(),
            marker_color=["#4c72b0", "#55a868", "#c44e52"],
        )
    ],
    layout=go.Layout(
        title="Average Body Mass of Penguins by Species",
        xaxis=dict(title="Species"),
        yaxis=dict(title="Body Mass (g)", tickformat=",.0f"),
    ),
)

maidr.show(fig)  

Dodged (Grouped) Bar Plot

Show the code
import plotly.graph_objects as go

import maidr  

fig = go.Figure(
    data=[
        go.Bar(name="Male", x=["Adelie", "Chinstrap", "Gentoo"], y=[4043, 3939, 5485]),
        go.Bar(
            name="Female", x=["Adelie", "Chinstrap", "Gentoo"], y=[3369, 3527, 4680]
        ),
    ],
    layout=go.Layout(
        title="Average Body Mass by Species and Sex",
        xaxis=dict(title="Species"),
        yaxis=dict(title="Body Mass (g)", tickformat=",.0f"),
        barmode="group",
    ),
)

maidr.show(fig)  

Stacked Bar Plot

Show the code
import plotly.graph_objects as go

import maidr  

fig = go.Figure(
    data=[
        go.Bar(
            name="Adelie", x=["Torgersen", "Biscoe", "Dream"], y=[47, 44, 56]
        ),
        go.Bar(
            name="Chinstrap", x=["Torgersen", "Biscoe", "Dream"], y=[0, 0, 68]
        ),
        go.Bar(
            name="Gentoo", x=["Torgersen", "Biscoe", "Dream"], y=[0, 124, 0]
        ),
    ],
    layout=go.Layout(
        title="Penguin Count by Island (Stacked)",
        xaxis=dict(title="Island"),
        yaxis=dict(title="Count", tickformat=".0f"),
        barmode="stack",
    ),
)

maidr.show(fig)  

Count Plot (Categorical Histogram)

Show the code
import plotly.graph_objects as go

import maidr  

# Count plot is a histogram with categorical data
species = [
    "Adelie", "Adelie", "Adelie", "Adelie", "Adelie",
    "Chinstrap", "Chinstrap", "Chinstrap",
    "Gentoo", "Gentoo", "Gentoo", "Gentoo",
]

fig = go.Figure(
    data=[go.Histogram(x=species)],
    layout=go.Layout(
        title="Penguin Species Count",
        xaxis=dict(title="Species"),
        yaxis=dict(title="Count", tickformat=".0f"),
    ),
)

maidr.show(fig)  

Pie Chart

A pie is one flat row of slices, so navigation runs left and right across them and there is no second dimension to move through. Each slice is announced by its label, its value, and the share of the whole that value works out to. The percentage is derived from the values at render time rather than authored, so it can never disagree with the numbers it is derived from.

Plotly draws a pie’s wedges largest first — sort defaults to true — so the order of the values array is not the order of the slices. maidr reproduces that ordering, because the slice a user hears has to be the slice that highlights. Pass sort=False to keep the authored order in both.

Plotly also lays the wedges out counterclockwise unless direction says otherwise, while maidr walks a pie clockwise: Right steps to the next slice the way a clock hand goes, the audio pans each slice to where it sits, and p names its clock position. The layer therefore declares the drawn direction, and the renderer walks the slices in reverse so that Right still moves clockwise with the highlight on the slice being read. It also says where the ring begins, which for a counterclockwise pie is not rotation alone: plotly ends the first wedge at rotation and draws it clockwise of there, so a default pie’s largest slice sits to the right of 12 o’clock and the ring starts where that slice ends. A renderer older than the one that reads these values ignores them and walks the slices as drawn.

Show the code
import plotly.graph_objects as go
import seaborn as sns

import maidr  

# Load the tips dataset
tips = sns.load_dataset("tips")

# Count the tips recorded on each day of the week
day_counts = tips["day"].value_counts()

fig = go.Figure(
    go.Pie( 
        labels=day_counts.index.tolist(),
        values=day_counts.values.tolist(),
        # Keep the authored order rather than letting plotly re-sort it.
        sort=False,
    )
)

fig.update_layout(
    title="Share of Tips by Day",
    # Name the two dimensions of a slice, so it is read out as
    # "Day: Sat, Number of tips: 87" rather than "Label: Sat, Value: 87".
    xaxis=dict(title="Day"), 
    yaxis=dict(title="Number of tips"), 
)

maidr.show(fig)  

A donut is the same chart with hole set: it changes the wedge geometry and leaves the data behind it untouched, so it is emitted as a pie layer too.

Note

Plotly names neither axis of a pie, so the layout’s xaxis/yaxis titles are what tell maidr how to announce a slice; without them it falls back to “Category” and “Value”, the same pair the matplotlib side uses.

Those titles are only borrowed when the figure holds nothing but pies. A cartesian trace that declares no axis pair of its own shares the same default xaxis/yaxis, and those titles describe its axes — so in a mixed figure the pie keeps the generic pair rather than announcing a bar’s axis names against its slices.

Plotly builds its own slice list before drawing, and maidr follows it exactly so that data index k and highlighted wedge k stay the same slice: entries plotly does not read as numbers draw no wedge, repeated labels merge into one wedge holding their sum, and a label listed in layout.hiddenlabels is dropped along with its wedge.

Histogram

Show the code
import plotly.graph_objects as go
import seaborn as sns

import maidr  

# Load the dataset
iris = sns.load_dataset("iris")

# Choose a column for the histogram
data = iris["petal_length"]

# Create the histogram plot
fig = go.Figure(
    data=[
        go.Histogram(
            x=data.tolist(),
            nbinsx=20,
            marker=dict(color="steelblue", line=dict(color="black", width=1)),
        )
    ],
    layout=go.Layout(
        title="Histogram of Petal Lengths in Iris Dataset",
        xaxis=dict(title="Petal Length (cm)", tickformat=".1f"),
        yaxis=dict(title="Frequency", tickformat=".0f"),
    ),
)

maidr.show(fig)  

Box Plot (Vertical)

Show the code
import numpy as np
import plotly.graph_objects as go

import maidr  

# Vertical box plot with three groups
np.random.seed(42)
data_group1 = np.append(np.random.normal(100, 10, 200), [150, 160, 50, 40])
data_group2 = np.append(np.random.normal(90, 20, 200), [180, 190, 10, 20])
data_group3 = np.append(np.random.normal(80, 30, 200), [200, 210, -10, -20])

fig = go.Figure()
fig.add_trace(
    go.Box(y=data_group1.tolist(), name="Group 1", marker_color="lightblue")
)
fig.add_trace(
    go.Box(y=data_group2.tolist(), name="Group 2", marker_color="lightgreen")
)
fig.add_trace(
    go.Box(y=data_group3.tolist(), name="Group 3", marker_color="tan")
)

fig.update_layout(
    title="Vertical Box Plot",
    xaxis=dict(title="Group"),
    yaxis=dict(title="Values", tickformat=".1f"),
)

maidr.show(fig)  

Box Plot (Horizontal)

Show the code
import numpy as np
import plotly.graph_objects as go

import maidr  

# Horizontal box plot with three groups
np.random.seed(42)

fig = go.Figure()
fig.add_trace(
    go.Box(
        x=np.random.normal(100, 10, 200).tolist(),
        name="Group 1",
        marker_color="lightblue",
    )
)
fig.add_trace(
    go.Box(
        x=np.random.normal(90, 20, 200).tolist(),
        name="Group 2",
        marker_color="lightgreen",
    )
)
fig.add_trace(
    go.Box(
        x=np.random.normal(80, 30, 200).tolist(),
        name="Group 3",
        marker_color="tan",
    )
)

fig.update_layout(
    title="Horizontal Box Plot",
    xaxis=dict(title="Values", tickformat=".1f"),
    yaxis=dict(title="Group"),
)

maidr.show(fig)  

Heatmap

Show the code
import numpy as np
import plotly.graph_objects as go

import maidr  

vegetables = [
    "cucumber",
    "tomato",
    "lettuce",
    "asparagus",
    "potato",
    "wheat",
    "barley",
]
farmers = [
    "Farmer Joe",
    "Upland Bros.",
    "Smith Gardening",
    "Agrifun",
    "Organiculture",
    "BioGoods Ltd.",
    "Cornylee Corp.",
]

harvest = np.array(
    [
        [0.8, 2.4, 2.5, 3.9, 0.0, 4.0, 0.0],
        [2.4, 0.0, 4.0, 1.0, 2.7, 0.0, 0.0],
        [1.1, 2.4, 0.8, 4.3, 1.9, 4.4, 0.0],
        [0.6, 0.0, 0.3, 0.0, 3.1, 0.0, 0.0],
        [0.7, 1.7, 0.6, 2.6, 2.2, 6.2, 0.0],
        [1.3, 1.2, 0.0, 0.0, 0.0, 3.2, 5.1],
        [0.1, 2.0, 0.0, 1.4, 0.0, 1.9, 6.3],
    ]
)

fig = go.Figure(
    data=[
        go.Heatmap(
            z=harvest.tolist(),
            x=farmers,
            y=vegetables,
            colorbar=dict(title="Tons/Year", tickformat=".1f"),
        )
    ],
    layout=go.Layout(
        title="Harvest of Local Farmers (in tons/year)",
        xaxis=dict(title="Farmers"),
        yaxis=dict(title="Vegetables"),
    ),
)

maidr.show(fig)  

Line Plot

Show the code
import plotly.graph_objects as go
import seaborn as sns

import maidr  

# Load the flights dataset from seaborn
flights = sns.load_dataset("flights")

# Pivot the dataset and compute total passengers per year
flights_wide = flights.pivot(index="year", columns="month", values="passengers")
flights_wide["Total"] = flights_wide.sum(axis=1)
flights_wide.reset_index(inplace=True)

# Plot the total number of passengers per year
fig = go.Figure(
    data=[
        go.Scatter(
            x=flights_wide["year"].tolist(),
            y=flights_wide["Total"].tolist(),
            mode="lines",
            name="Total Passengers",
        )
    ],
    layout=go.Layout(
        title="Total Passengers per Year",
        xaxis=dict(title="Year"),
        yaxis=dict(title="Total Passengers (Thousands)", tickformat=",.0f"),
    ),
)

maidr.show(fig)  

Multi-Line Plot

Show the code
import plotly.graph_objects as go

import maidr  

fig = go.Figure(
    data=[
        go.Scatter(
            x=[2019, 2020, 2021, 2022, 2023],
            y=[100, 120, 115, 140, 160],
            mode="lines",
            name="Product A",
        ),
        go.Scatter(
            x=[2019, 2020, 2021, 2022, 2023],
            y=[80, 95, 110, 105, 130],
            mode="lines",
            name="Product B",
        ),
        go.Scatter(
            x=[2019, 2020, 2021, 2022, 2023],
            y=[60, 70, 85, 90, 100],
            mode="lines",
            name="Product C",
        ),
    ],
    layout=go.Layout(
        title="Sales Trends by Product",
        xaxis=dict(title="Year"),
        yaxis=dict(title="Sales (units)", tickformat=",.0f"),
    ),
)

maidr.show(fig)  

Step Plot

Plotly has no step trace type. A staircase is an ordinary Scatter trace whose line_shape tells plotly.js to draw risers between the samples rather than interpolating across them, so line_shape is the only thing that marks the data as piecewise constant. maidr reads it and exports a step layer.

A hypnogram is the canonical case: the sleep stage is held for a stretch of the night and then jumps. Drawn as a line it would claim the sleeper glided through every intermediate stage.

Show the code
import plotly.graph_objects as go

import maidr  

# Ordinal sleep stages, deepest first, so that "up" means lighter sleep.
STAGE_NAMES = ["N3", "N2", "N1", "REM", "Awake"]

hours = [i * 0.5 for i in range(17)]
stages = [4, 3, 2, 1, 0, 0, 1, 3, 2, 1, 0, 1, 3, 2, 1, 3, 4]

fig = go.Figure(
    go.Scatter(
        x=hours,
        y=stages,
        mode="lines",
        line_shape="hv",  
        name="Night 1",
    )
)

fig.update_layout(
    title="Hypnogram<br>Sleep stage across one night",
    xaxis_title="Time asleep (hours)",
    yaxis=dict(
        title="Sleep stage",
        tickmode="array",
        tickvals=list(range(len(STAGE_NAMES))),
        ticktext=STAGE_NAMES,
    ),
)

maidr.show(fig)

line_shape decides which side of each sample the value is held on, and maidr reports it so the description can say what the chart actually means:

Plotly line_shape maidr stepDirection Meaning
"hv" hv Value holds until the next x value, then jumps
"vh" vh Value jumps at the current x value, then holds
"hvh" mid Value jumps midway between x values
"vhv" (none) Still a step, but no direction is claimed — see below
Note

"vhv" is the one shape whose horizontal segments do not sit at a sample’s own value, so it has no maidr equivalent. The trace still binds as a step, because the data is piecewise constant, but no stepDirection is emitted rather than one being guessed.

A maidr layer carries a single stepDirection for all of its series, so traces drawn with different shapes are exported as separate layers. Merging an "hv" trace with a "vh" one would describe one of them wrongly.

Scatter Plot

Show the code
import plotly.graph_objects as go
import seaborn as sns

import maidr  

# Load the Iris dataset
iris = sns.load_dataset("iris")

# Plot sepal_length vs sepal_width
fig = go.Figure(
    data=[
        go.Scatter(
            x=iris["sepal_length"].tolist(),
            y=iris["sepal_width"].tolist(),
            mode="markers",
            marker=dict(color="blue", size=6),
            name="Iris Data Points",
        )
    ],
    layout=go.Layout(
        title="Iris Dataset: Sepal Length vs Sepal Width",
        xaxis=dict(title="Sepal Length (cm)", tickformat=".1f"),
        yaxis=dict(title="Sepal Width (cm)", tickformat=".1f"),
    ),
)

maidr.show(fig)  

KDE (Histogram + KDE Overlay)

Show the code
import numpy as np
import plotly.graph_objects as go
from scipy.stats import gaussian_kde

import maidr  

# Generate sample data
np.random.seed(42)
data = np.concatenate([np.random.normal(0, 1, 300), np.random.normal(4, 1.5, 200)])

# Compute KDE
kde = gaussian_kde(data)
x_range = np.linspace(data.min() - 1, data.max() + 1, 200)
kde_values = kde(x_range)

# Create histogram + KDE overlay
fig = go.Figure()
fig.add_trace(
    go.Histogram(
        x=data.tolist(),
        nbinsx=30,
        histnorm="probability density",
        name="Histogram",
        opacity=0.6,
    )
)
fig.add_trace(
    go.Scatter(
        x=x_range.tolist(),
        y=kde_values.tolist(),
        mode="lines",
        name="KDE",
        line=dict(color="red", width=2),
    )
)
fig.update_layout(
    title="Histogram with KDE Overlay",
    xaxis=dict(title="Value", tickformat=".1f"),
    yaxis=dict(title="Density", tickformat=".3f"),
)

maidr.show(fig)  

Regression (Scatter + Trend Line)

Show the code
import numpy as np
import plotly.graph_objects as go

import maidr  

# Generate sample data with a linear trend
np.random.seed(42)
x = np.linspace(0, 10, 50)
y = 2.5 * x + 3 + np.random.normal(0, 2, 50)

# Compute linear regression
coeffs = np.polyfit(x, y, 1)
trend_y = np.polyval(coeffs, x)

# Create scatter plot with regression line
fig = go.Figure()
fig.add_trace(
    go.Scatter(
        x=x.tolist(),
        y=y.tolist(),
        mode="markers",
        name="Data",
        marker=dict(color="steelblue"),
    )
)
fig.add_trace(
    go.Scatter(
        x=x.tolist(),
        y=trend_y.tolist(),
        mode="lines",
        name="Regression",
        line=dict(color="red", width=2),
    )
)
fig.update_layout(
    title="Scatter Plot with Regression Line",
    xaxis=dict(title="X", tickformat=".1f"),
    yaxis=dict(title="Y", tickformat=".2f"),
)

maidr.show(fig)  

Multipanel Plot (Line + Bar + Scatter)

Show the code
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots

import maidr  

# Data for line plot
x_line = np.array([1, 2, 3, 4, 5, 6, 7, 8])
y_line = np.array([2, 4, 1, 5, 3, 7, 6, 8])

# Data for bar plot
np.random.seed(42)
categories = ["A", "B", "C", "D", "E"]
values = (np.random.rand(5) * 10).tolist()

# Data for scatter plot
np.random.seed(42)
x_scatter = np.random.randn(50).tolist()
y_scatter = np.random.randn(50).tolist()

# Create a figure with 3 subplots arranged vertically
fig = make_subplots(
    rows=3,
    cols=1,
    subplot_titles=("Line Plot: Random Data", "Bar Plot: Random Values", "Scatter Plot"),
)

# First panel: Line plot
fig.add_trace(
    go.Scatter(
        x=x_line.tolist(),
        y=y_line.tolist(),
        mode="lines",
        name="Line Data",
        line=dict(color="blue", width=2),
    ),
    row=1,
    col=1,
)

# Second panel: Bar plot
fig.add_trace(
    go.Bar(
        x=categories,
        y=values,
        name="Bar Data",
        marker_color="green",
        opacity=0.7,
    ),
    row=2,
    col=1,
)

# Third panel: Scatter plot
fig.add_trace(
    go.Scatter(
        x=x_scatter,
        y=y_scatter,
        mode="markers",
        name="Scatter Data",
        marker=dict(color="red", size=6),
    ),
    row=3,
    col=1,
)

# Update axes labels and formatting
fig.update_xaxes(title_text="X-axis", tickformat=".0f", row=1, col=1)
fig.update_yaxes(title_text="Values", tickformat=".0f", row=1, col=1)
fig.update_xaxes(title_text="Categories", row=2, col=1)
fig.update_yaxes(title_text="Values", tickformat=".1f", row=2, col=1)
fig.update_xaxes(title_text="X", tickformat=".1f", row=3, col=1)
fig.update_yaxes(title_text="Y", tickformat=".1f", row=3, col=1)

fig.update_layout(
    title="Multipanel Plot: Line, Bar, and Scatter",
    height=900,
    showlegend=False,
)

maidr.show(fig)  

Facet Bar Plot (2x2 Grid)

Show the code
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots

import maidr  

categories = ["A", "B", "C", "D", "E"]

np.random.seed(42)
data_group1 = (np.random.rand(5) * 10).tolist()
data_group2 = (np.random.rand(5) * 100).tolist()
data_group3 = (np.random.rand(5) * 36).tolist()
data_group4 = (np.random.rand(5) * 42).tolist()

data_sets = [data_group1, data_group2, data_group3, data_group4]
condition_names = ["Group 1", "Group 2", "Group 3", "Group 4"]

# Compute consistent y-axis limits across all groups
all_data = np.concatenate(data_sets)
y_min = float(np.min(all_data) * 0.9)
y_max = float(np.max(all_data) * 1.1)

# Colors matching matplotlib's default cycle
colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728"]

# Create a 2x2 faceted layout
fig = make_subplots(
    rows=2,
    cols=2,
    subplot_titles=tuple(condition_names),
    shared_xaxes=True,
    shared_yaxes=True,
)

# Add a bar trace to each subplot
for i, (data, condition) in enumerate(zip(data_sets, condition_names)):
    row = i // 2 + 1
    col = i % 2 + 1
    fig.add_trace(
        go.Bar(
            x=categories,
            y=data,
            name=condition,
            marker_color=colors[i],
            opacity=0.7,
            text=[f"{v:.1f}" for v in data],
            textposition="outside",
        ),
        row=row,
        col=col,
    )

# Apply consistent y-axis range and tick formatting
for i in range(1, 5):
    yaxis_key = f"yaxis{i}" if i > 1 else "yaxis"
    fig.layout[yaxis_key].update(range=[y_min, y_max], tickformat=".1f")

# Axes labels
fig.update_xaxes(title_text="Categories", row=2, col=1)
fig.update_xaxes(title_text="Categories", row=2, col=2)
fig.update_yaxes(title_text="Values", row=1, col=1)
fig.update_yaxes(title_text="Values", row=2, col=1)

fig.update_layout(
    title="Facet Plot: Bar Charts by Condition",
    height=700,
    showlegend=False,
)

maidr.show(fig)  

Facet Combined Plot (Line + Bar)

Show the code
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots

import maidr  

# Generate data
num_rows = 3
num_points = 5
x = np.arange(num_points)

np.random.seed(42)
line_data = [np.random.rand(num_points) * 10 for _ in range(num_rows)]
bar_data = [np.abs(np.sin(x + i) * 5) + i for i in range(num_rows)]

# Build subplot titles
subplot_titles = []
for r in range(1, num_rows + 1):
    subplot_titles.extend([f"Line Plot {r}", f"Bar Plot {r}"])

# Colors matching matplotlib's default cycle
colors = ["#1f77b4", "#ff7f0e", "#2ca02c"]

# Create a num_rows x 2 faceted layout (line plots left, bar plots right)
fig = make_subplots(
    rows=num_rows,
    cols=2,
    subplot_titles=tuple(subplot_titles),
    shared_yaxes="rows",
)

for row in range(num_rows):
    # Left column: line plot
    fig.add_trace(
        go.Scatter(
            x=x.tolist(),
            y=line_data[row].tolist(),
            mode="lines+markers",
            name=f"Line {row + 1}",
            line=dict(color=colors[row], width=2),
            marker=dict(size=6),
        ),
        row=row + 1,
        col=1,
    )

    # Right column: bar plot
    fig.add_trace(
        go.Bar(
            x=x.tolist(),
            y=bar_data[row].tolist(),
            name=f"Bar {row + 1}",
            marker_color=colors[row],
            opacity=0.7,
        ),
        row=row + 1,
        col=2,
    )

    # Y-axis label on left column only
    fig.update_yaxes(
        title_text=f"Values (Row {row + 1})",
        tickformat=".1f",
        row=row + 1,
        col=1,
    )
    fig.update_yaxes(tickformat=".1f", row=row + 1, col=2)

# X-axis labels on bottom row
fig.update_xaxes(title_text="X Values", tickformat=".0f", row=num_rows, col=1)
fig.update_xaxes(title_text="X Values", tickformat=".0f", row=num_rows, col=2)

fig.update_layout(
    title="Facet Plot Example with Shared Y-Axes",
    height=300 * num_rows,
    showlegend=False,
)

maidr.show(fig)  

Precision-Recall Curve [experimental]

A precision-recall curve is drawn as an ordinary line, so a trace says it is one in its meta, the slot plotly keeps for your own metadata: meta={"maidr": {"type": "pr_curve"}}. This is the declaration maidr.js reads on a plotly chart in the browser too. Every other line on the subplot joins the chart as a further curve, unless the block says "merge": False. The block can name the customdata key holding each point’s decision threshold, and give the share of positives (prevalence) and the average precision (ap), both as fractions of one. Each point is then announced with its threshold and how far its precision sits above the share of positives; see the precision-recall page for the reading. PR_CURVE is experimental: see Plot Type Stability.

Show the code
import plotly.graph_objects as go

import maidr  

recall = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]
thresholds = [0.95, 0.8, 0.6, 0.45, 0.3, 0.1]

fig = go.Figure(
    [
        go.Scatter(
            x=recall,
            y=[1.0, 0.94, 0.88, 0.79, 0.66, 0.42],
            name="Gradient boosting",
            mode="lines",
            customdata=[{"threshold": t} for t in thresholds],
            meta={  
                "maidr": {"type": "pr_curve", "prevalence": 0.3, "ap": 0.81}  
            },  
        ),
        go.Scatter(
            x=recall,
            y=[1.0, 0.85, 0.74, 0.63, 0.52, 0.38],
            name="Logistic regression",
            mode="lines",
        ),
    ],
    layout=go.Layout(
        title="Precision-recall curves",
        xaxis=dict(title="Recall"),
        yaxis=dict(title="Precision"),
    ),
)

maidr.show(fig)  

Percentile Band [experimental]

plotly has no fan chart of its own: a band is a trace filled "tonexty" to the trace before it, and nothing says which quantiles its edges are. The median says so in its meta, naming each band’s filled trace by its name or uid and the two levels its edges are. The traces become one percentile band: Left and Right move along x, each x is entered on the median, and Up and Down move out to each band’s bounds, announced with the share of the distribution they hold (“Middle 90% is …”). The bands must nest, each inside the next wider one. PERCENTILE_BAND is experimental: see Plot Type Stability.

Show the code
import plotly.graph_objects as go

import maidr  

weeks = list(range(1, 9))
median = [120, 126, 131, 135, 142, 147, 150, 156]


def edge(spread, name, fill=None):
    return go.Scatter(
        x=weeks,
        y=[m + spread * week for m, week in zip(median, weeks)],
        name=name,
        mode="lines",
        line_width=0,
        fill=fill,
        fillcolor="rgba(31, 119, 180, 0.2)",
        showlegend=fill is not None,
    )


fig = go.Figure(
    [
        edge(-3.3, "5th percentile"),
        edge(3.3, "90% band", fill="tonexty"),
        edge(-1.4, "25th percentile"),
        edge(1.4, "50% band", fill="tonexty"),
        go.Scatter(
            x=weeks,
            y=median,
            name="Median",
            mode="lines",
            meta={  
                "maidr": {  
                    "type": "percentile_band",  
                    "bands": [  
                        {"series": "90% band", "lower": 0.05, "upper": 0.95},  
                        {"series": "50% band", "lower": 0.25, "upper": 0.75},  
                    ],  
                }  
            },  
        ),
    ],
    layout=go.Layout(
        title="Weekly demand forecast",
        xaxis=dict(title="Week"),
        yaxis=dict(title="Units"),
    ),
)

maidr.show(fig)