Search code examples
pythonmatplotlibpixeldpi

Draw evenly spaced vertical lines on defined pixel position


Problem Description

I am trying to plot a series (length n) of tuples (low, high) as vertical lines (length = high-low) on a plotting area which must be 3n pixels wide. Each vertical line must only have a width of 1 pixel and the spacing between the lines must be exactly 2 pixels. However, the vertical lines are not equally spaced.

What I was able to produce so far:

  • Plot with the needed pixel width and height based on my system's dpi (documentation)
  • Save the figure in the same pixel dimensions
  • Plot the vertical lines of width 1px on the given x-positions

The problem arises in the last point. The spacing between the lines is not consistently 2 pixel, probably since the defined plot size defines the size of the whole figure and not only the size of the main plotting area.

I already tried the following post:

Example

Given a sequence of 7 tuples (n=7), each tuple being of the form (low, high) I would like to plot a figure 21 (3n) pixels wide. Thus, the plot should contain 7 vertical lines, each stretching from the low to high value on the y-axis. The axes should be excluded in the final plot. The x-positions for the lines should be at pixel 2,5,8,11,14,17 and 20.

The problem is, that matplotlib does not seem to preserve the defined spacing, probably due to the implicitly added boundary around the figure (see picture below).

The code at the end generates this graph (zoomed png):

Example Result of Plots

The plot shows which problems should be solved:

  • Inconsistent spacing between the bars (i.e. the pixel spacing between line 2 and 3 should be 2 instead of 1).
  • The main plotting area must be 3n pixels wide, not the whole figure.

Code for example:

from pathlib import Path
import matplotlib.pyplot as plt
from ctypes import windll
import numpy as np

plot_path = Path("./plots/testing/")
plot_path.mkdir(parents=True, exist_ok=True)
SYS_DPI = windll.user32.GetDpiForSystem()
print(f"Recognized DPI: {SYS_DPI}")

# create example dataset
example_series = [(1.3, 4.2), (5.3, 8.9), (4.0, 5.5),
                  (6, 7.8), (3.2, 4.5), (5.1, 8.1), (3, 8)]
width_pixel = len(example_series)*3
height_pixel = 10

# define xlims
y_min_vals = [x[0] for x in example_series]
y_vals_vals = [x[1] for x in example_series]
min_y, max_y = min(y_min_vals), max(y_vals_vals)

# define x positions and xlims
x_pos = np.arange(0, width_pixel, step=3)+2  # 2,5,8,11,14,17,20
min_x, max_x = 0, width_pixel

# create plot of needed size
fix, ax = plt.subplots(
    figsize=(width_pixel/SYS_DPI, height_pixel/SYS_DPI), dpi=SYS_DPI)
ax.set_facecolor("white")
ax.set_axis_off()
ax.set_xlim((min_x, max_x))
ax.set_ylim(min_y, max_y)
for idx, val in enumerate(example_series):
    print(f"{idx}: values: {val}")
    ax.plot([x_pos[idx], x_pos[idx]], [val[0], val[1]],
            color="black", linewidth=1)

plt.savefig(plot_path/"test.png",
            bbox_inches=None,
            pad_inches=0,
            transparent=False,
            dpi=SYS_DPI,
            facecolor="white")

Solution

  • I obtain picture you want by changing aspect ratio of the axes to aspect ratio of figure and setting relative axes position to (0,0,1,1).

    Enlarging screenshot of what i get (resolution 21*10 pixels)

    enter image description here

    example_series = [(1.3, 4.2), (5.3, 8.9), (4.0, 5.5),
                      (6, 7.8), (3.2, 4.5), (5.1, 8.1), (3, 8)]
    width_pixel = len(example_series)*3
    height_pixel = 10
    dpi = 72 # random dpi because i don't have windows. Work with other values too.
    
    bottom=np.array([x[0] for x in example_series])
    top = np.array([x[1] for x in example_series])
    height = top - bottom
    
    min_y, max_y = min(bottom), max(top)
    min_x, max_x = 0, width_pixel
    
    x_pos = np.array([2,5,8,11,14,17,20])-1
    
    fig_width = width_pixel/dpi
    fig_height = height_pixel/dpi
    fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=dpi)
    
    ax.set_xlim(min_x, max_x)
    ax.set_ylim(min_y, max_y)
    
    # WHY IT WORK
    ax.set_box_aspect(fig_height/fig_width)
    ax.set_position([0.0,0.0,1.0,1.0])
    ax.set_axis_off()
    
    # Bar instead of lines. It looks more clean for me
    ax.bar(x_pos, height, width=1.0, bottom=bottom, align='edge', color='black')
    
    plt.savefig('test.png')