Plotly Accessible Plot Examples

Code examples showing how to make Plotly bar charts, histograms, line charts, box plots, heatmaps, and scatter plots 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

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

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

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)

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.

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

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)

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)

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

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

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

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.

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

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)

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)

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)

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)

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)

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)