2025, Sep 22 21:00

Cut a Clean Gap in a Matplotlib Line at a Chosen Data Point: Stop Using Dash Patterns, Use NaN Masking

Learn how to create a single, precise gap in a Matplotlib line at a data coordinate. Avoid fragile dash patterns measured in points; use NaN masking instead.

Placing a single, precisely located gap in a Matplotlib line by tweaking the dash pattern looks simple until coordinate spaces get involved. When the goal is a clean break at a chosen data coordinate, manipulating dashes in display space quickly turns brittle. There is a more robust way to get the same visual result without guessing at pixel distances.

Problem setup

The task is to compute a dash sequence such that a solid line contains one gap centered at a specific point on that line. The initial approach transforms data coordinates to display space and computes distances there to build the dash sequence.

import matplotlib.pyplot as mp
from matplotlib.lines import Line2D
import numpy as np
def compute_gap_dashes(ax_obj, seg, hit_pt):
    to_disp = lambda xy: ax_obj.transData.transform(xy)
    x_d, y_d = to_disp(np.array(seg.get_data()).T).T
    xh, yh = to_disp(hit_pt)
    dist_start = np.sqrt((x_d[0] - xh) ** 2 + (y_d[0] - yh) ** 2)
    full_len = np.sqrt((x_d[0] - x_d[1]) ** 2 + (y_d[0] - y_d[1]) ** 2)
    gap_size = full_len / 10
    pattern = (dist_start - (gap_size / 2), gap_size, full_len)
    return pattern
fig, axs = mp.subplots()
ln = Line2D((0, 1), (0, 2), zorder=0)
axs.add_line(ln)
target = (0.3, 0.6)
axs.scatter(*ln.get_data(), c='g', zorder=1)
axs.scatter(*target, c='r', zorder=1)
dash_pattern = compute_gap_dashes(axs, ln, target)
ln.set_dashes(dash_pattern)

Why the gap drifts away

Dash sequences in Matplotlib are measured in points, not pixels. When distances are mixed between data coordinates and display coordinates, the resulting pattern often won’t align with the intended data position. Even with careful transforms, trying to center a gap at a given data coordinate by nudging a dash array is fragile. Minor changes to figure size, DPI, or transformations shift the actual break.

A cleaner approach: cut the line with nan masking

Instead of negotiating dash spacing, insert two boundary vertices around the target point and use nan to break the path. This produces a single plotted line with a real gap around the chosen data coordinate. The gap width is expressed as a proportion of the x-distance between the neighboring vertices around the target, and the line break is created by assigning nan to the target’s y-value.

import matplotlib.pyplot as mp
import numpy as np
def carve_gap_limits(verts, idx_hit, hole_frac=0.1):
    """
    Create two boundary points around verts[idx_hit] so that
    the gap spans hole_frac of the x-distance between the
    previous and next vertices.
    """
    hit_xy = verts[idx_hit]
    prev_xy = verts[idx_hit - 1]
    next_xy = verts[idx_hit + 1]
    pos_rel = (hit_xy[0] - prev_xy[0]) / (next_xy[0] - prev_xy[0])
    left_rel = max(0, pos_rel - hole_frac / 2)
    right_rel = min(1, pos_rel + hole_frac / 2)
    left_xy = (1 - left_rel) * prev_xy + left_rel * next_xy
    right_xy = (1 - right_rel) * prev_xy + right_rel * next_xy
    return left_xy, right_xy
pts = np.array([[0, 0], [0.3, 0.6], [1, 2]])
idx = 1  # index of the target point within pts
hole_frac = 0.1  # proportion for the gap width
is_cross = np.arange(len(pts)) == idx
left_xy, right_xy = carve_gap_limits(pts, idx, hole_frac=hole_frac)
aug = pts.copy()
aug[idx, 1] = np.nan  # suppress the segment around the target point
aug = np.insert(aug, [idx, idx + 1], [left_xy, right_xy], axis=0)
fig, ax = mp.subplots()
ax.plot(*aug.T)
ax.scatter(*pts[~is_cross, :].T, c="g")
ax.scatter(*pts[is_cross, :].T, c="r")
mp.show()

This produces the intended gap where you want it, without adjusting dash arrays or worrying about display-space distances.

Why this is worth knowing

Relying on dash spacing conflates coordinate systems and makes the visual fragile. Expressing the gap directly in data coordinates keeps the intent local to the data and avoids surprises from figure or DPI changes. There is also a practical benefit: with nan masking the entire broken line is handled as a single line object, which keeps the plotting logic straightforward. If you instead decide to draw piecewise solid segments, drawing multiple lines works, and a LineCollection can be more efficient than piling up many Line2D objects.

Conclusion

When a line needs a deliberate break at a specific data coordinate, avoid manipulating dash patterns. Insert boundary vertices around the target and use nan masking to cut the path. Remember that Matplotlib dash sequences are specified in points, and mixing spaces is what makes the dash-based approach unreliable. Keep the gap logic in data space, and reach for a LineCollection only if you move to piecewise segments and need the efficiency of a batched collection.

The article is based on a question from StackOverflow by Macklin Entricken and an answer by Ben Grossmann.