Accessible Line and Step Plots in matplotlib and seaborn with py-maidr

Make matplotlib and seaborn line, multi-line and step plots accessible with py-maidr: pitch follows the value along x and each point is announced.

A line plot is the chart sonification was made for: the Left and Right arrows walk the points in x order and the tone’s pitch rises and falls with y, so the shape of the series can be heard in one autoplay pass (Ctrl or Cmd + Shift + Right). A multi-line plot keeps that reading per series and lets the reader move between the series. A step plot is a line whose value is held across an interval and then jumps; py-maidr reports which side of each sample the value is held on and announces named tick labels in place of numeric codes.

LINE and STEP are in the stable set: see Plot Type Stability.

Setup

Every example on this page needs only the import below. The plot cells repeat it so each one can be copied on its own.

import matplotlib.pyplot as plt

# Just import maidr package: plt.show() now renders accessible output 
import maidr  

Line Plot

Single Line Plot

import matplotlib.pyplot as plt
import seaborn as sns

import maidr 


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

# Choose a specific subset of the dataset (e.g., data for 'Thursday')
subset_data = tips[tips["day"] == "Thur"]

# Create a line plot
fig, ax = plt.subplots(figsize=(6, 6))
line_plot = sns.lineplot( 
    data=subset_data,
    x="total_bill",
    y="tip",
    markers=True,
    style="day",
    legend=False,
    ax=ax,
)
ax.set_title("Tips vs Total Bill (Thursday)")
ax.set_xlabel("Total Bill")
ax.set_ylabel("Tip")

# Add currency formatters for better screen reader output
ax.xaxis.set_major_formatter("${x:.2f}")
ax.yaxis.set_major_formatter("${x:.2f}")

plt.show() 

Multiline Plot

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns

import maidr 
# Create sample data points
x = np.array([1, 2, 3, 4, 5, 6, 7, 8])
y1 = np.array([2, 4, 1, 5, 3, 7, 6, 8])
y2 = np.array([1, 3, 5, 2, 4, 6, 8, 7])
y3 = np.array([3, 1, 4, 6, 5, 2, 4, 5])

# Convert to pandas DataFrame for seaborn
data = pd.DataFrame(
    {
        "x": np.tile(x, 3),
        "y": np.concatenate([y1, y2, y3]),
        "series": np.repeat(["Series 1", "Series 2", "Series 3"], len(x)),
    }
)

# Create the plot
fig, ax = plt.subplots(figsize=(6, 6))

# Use seaborn lineplot for multiple lines
lineplot = sns.lineplot(
    x="x", y="y", hue="series", style="series", markers=True, dashes=True, data=data, ax=ax
)

# Customize the plot
ax.set_title("Seaborn Multiline Plot")
ax.set_xlabel("X values")
ax.set_ylabel("Y values")

# Add number formatters for better screen reader output
ax.xaxis.set_major_formatter("{x:.0f}")
ax.yaxis.set_major_formatter("{x:.0f}")

# Display the plot
plt.show() 

Step Plot

A step plot is the right chart when the value is piecewise constant: it is held across an interval and then jumps, rather than sliding continuously between samples. ax.step() produces one, as does any drawstyle="steps-*" passed to ax.plot() or sns.lineplot().

The classic case is a hypnogram — an ordinal sleep stage against time. Plot the stages as numeric codes so they keep driving sonification, braille and the min/max bounds, then name the codes with set_yticks(..., labels=...). maidr attaches the names to each point and announces “REM” instead of “3”.

import matplotlib.pyplot as plt

import maidr 

# Ordinal sleep stages, deepest first, so "up" means lighter sleep.
stage_codes = [0, 1, 2, 3, 4]
stage_names = ["N3", "N2", "N1", "REM", "Awake"]

# One reading every half hour across a night's sleep.
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, ax = plt.subplots(figsize=(10, 5))

# where="post": the stage holds until the next reading, then jumps.
ax.step(hours, stages, where="post") 

# Name the ordinal levels; the underlying y values stay numeric.
ax.set_yticks(stage_codes, labels=stage_names) 

ax.set_title("Hypnogram\nSleep stage across one night")
ax.set_xlabel("Time asleep (hours)")
ax.set_ylabel("Sleep stage")

# Add a number formatter for better screen reader output
ax.xaxis.set_major_formatter("{x:.1f}")

plt.show() 

The where argument 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:

matplotlib maidr stepDirection Meaning
where="post" (drawstyle="steps-post") hv Value holds until the next x value, then jumps
where="pre" (drawstyle="steps-pre" or "steps") vh Value jumps at the current x value, then holds
where="mid" (drawstyle="steps-mid") mid Value jumps midway between x values
Note

Level names are read from the y tick labels at render time, so calling set_yticks() or set_yticklabels() after plotting works as expected. A tick label that is itself a number ("3", "1,000", or the rescaled "0.0" a default axis prints for y = 1000000) carries no ordinal information and is not emitted, so a purely numeric step plot is announced by its numbers as usual.