Accessible Candlestick and Gantt Charts in matplotlib with py-maidr

Make mplfinance candlestick charts and matplotlib broken_barh Gantt charts accessible with py-maidr: OHLC values and task spans read by keyboard.

Both of these charts draw a span rather than a point. A candlestick chart (drawn with mplfinance) is read one trading day at a time: Left and Right move along the dates and each stop announces the open, high, low and close, with the volume panel available as its own subplot. A Gantt chart drawn with ax.broken_barh() is read as lanes of spans, so a reader hears when a task starts, when it ends and therefore how long it runs.

CANDLESTICK is in the stable set. GANTT is experimental and may change without a deprecation period: 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.

Show the code
import matplotlib.pyplot as plt

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

Candlestick Chart

Show the code
import mplfinance as mpf
import pandas as pd

import maidr 

# Load the sample data
daily = pd.read_csv("../../example/candle_stick/volcandat.csv", index_col=0, parse_dates=True)

# Create the candlestick chart with moving averages and volume
fig, axlist = mpf.plot(
    daily,
    type="candle",
    volume=True,
    mav = (3,6,9),
    returnfig=True,
    ylabel="Price",
    ylabel_lower="Volume",
    xlabel="Date",
    title="Stock Price with Volume",
    # Dates drawn, and announced, like "Nov 01, 2019"
    datetime_format="%b %d, %Y", 
)  

# Add formatters for better screen reader output
# Price axis - Currency format
axlist[0].yaxis.set_major_formatter("${x:,.2f}")
# Volume axis - Integer with thousands separator
axlist[2].yaxis.set_major_formatter("{x:,.0f}")

fig.tight_layout()

# Display with maidr
plt.show() 

Pass datetime_format= to mpf.plot(), as above, and the dates are announced the way the x axis draws them: every candle, volume bar and moving average reads as its tick label does, Nov 01, 2019 here, or 09:30 for minute bars drawn with "%H:%M". Any strftime() pattern works, so "%a %d %b" leaves the year out. Without it, each date is announced as the frame holds it, whatever format mplfinance picked for the ticks: 2019-11-01 for daily data like this, whose index has no time of day, and the full stamp, 2026-03-02 09:30:00, for intraday bars.

Set the format there rather than with a DateFormatter on the axis afterwards. mplfinance places candles by row number, not by date, so a DateFormatter reads those row numbers as days since 1970 and labels the axis with dates around Jan 01, 1970.

Gantt Chart [experimental]

WarningPrototype

This is one of the experimental plot types. It has not been through a user study, and it may change without a deprecation period. See Plot type stability.

ax.broken_barh() draws horizontal bars that start and stop along an axis, which is what a schedule looks like. maidr reads it as a gantt: every bar is a span, carrying both its start and its end rather than a single position, so a reader hears when a task begins, when it finishes, and therefore how long it runs.

The whole chart is one layer, organized into lanes. Each broken_barh() call contributes one lane, and the spans passed in that call are the spans of that lane — so a phase that stops and restarts is two spans on one lane, not two separate rows.

Show the code
import matplotlib.pyplot as plt

import maidr 

fig, ax = plt.subplots(figsize=(8, 4))

# One call is one lane. A lane can hold several spans: "Build" runs for four
# weeks, pauses for one, then resumes for three.
ax.broken_barh([(0, 3)], (10, 6)) 
ax.broken_barh([(3, 4), (8, 3)], (20, 6)) 
ax.broken_barh([(11, 2)], (30, 6)) 

ax.set_xlabel("Week")
ax.set_ylabel("Phase")
ax.set_yticks([13, 23, 33])
ax.set_yticklabels(["Design", "Build", "Launch"])
ax.set_title("Project schedule")

plt.show() 

Naming the lanes takes both set_yticks() and set_yticklabels(), as the example does. Labels alone are ignored, because a lane name is only trusted when the axis carries fixed tick positions to attach it to; otherwise a lane falls back to the center of the row it was drawn on — the (10, 6) above puts one at 13. Measured on the three lanes above:

what the axis sets lanes announced as
set_yticks() and set_yticklabels() Design, Build, Launch
set_yticklabels() alone 13.0, 23.0, 33.0
set_yticks() alone 13, 23, 33
neither 13.0, 23.0, 33.0

label= is the argument that looks like it should do this, and it does not — it adds a legend entry and leaves the lane unnamed.