pythonnumpymatplotlibplot

How can i set the ticks of ax and ax2 to the same position but I have different scales?


I have this python code snippet that sets the ticks of the two y-axes. But I can't figure out how I can synchronize the axe's scales.

if y_data_left:
        all_left = pd.concat([y for _, y, _, _ in y_data_left])
        ymin_left, ymax_left = all_left.min(), all_left.max()
        if ymin_left > 0:
            ymin_left = 0
        if ymin_left == ymax_left or ymax_left - ymin_left <= 0.01:
            ymax_left += 1
        margin_left = ymax_left * 1.1
        ax.set_ylim(ymin_left, margin_left)
        major_y_left = get_nice_tick_interval(ymin_left, ymax_left, 5) / tick_factor
        ax.yaxis.set_major_locator(ticker.MultipleLocator(major_y_left))
        ax.yaxis.set_minor_locator(ticker.MultipleLocator(major_y_left / 5))

    fig.canvas.draw()

    # Y-Achse rechts unabhängig skalieren, aber gleiche Positionen übernehmen
    if ax2 and y_data_right:
        all_right = pd.concat([y for _, y, _, _ in y_data_right])

        ymin_right = 0 if ymin_right > 0 else all_right.min()
        ymax_right = all_right.max()

        ax2.set_ylim(ymin_right, ymax_right)

        major_left = ax.get_yticks()
        n_ticks = len(major_left)

        major_right_interval = get_nice_tick_interval(ymin_right, ymax_right, n_ticks - 1) 
        yt_right = np.arange(ymin_right, ymax_right + major_right_interval, major_right_interval)
        ax2.set_yticks(yt_right)

        # Set Minor-Ticks
        if n_ticks > 1:
            minor_interval = major_right_interval / 5
            ax2.yaxis.set_minor_locator(MultipleLocator(minor_interval))

        ax2.tick_params(axis="y", labelright=True)

Here is the get_nice_tick_interval function:

def get_nice_tick_interval(vmin, vmax, target_steps=5, factor=1.0):
    target_steps *= factor
    if vmin == vmax:
        vmin -= 1
        vmax += 1
    raw_interval = (vmax - vmin) / target_steps
    exponent = np.floor(np.log10(raw_interval))
    fraction = raw_interval / 10**exponent
    if fraction <= 1:
        nice_fraction = 1
    elif fraction <= 2:
        nice_fraction = 2
    elif fraction <= 5:
        nice_fraction = 5
    else:
        nice_fraction = 10
    return nice_fraction * 10**exponent

I have tried to get the pixels of the plot like right_ticks = ax2.transData.inverted().transform(np.column_stack([np.zeros_like(left_pixels), left_pixels]))[:, 1] but that didn't work. I hoped that the ticks would be across from each other but that hasn't been the case.


Solution

  • Don’t mess with pixels. Take the left ticks’ relative positions (0→1 along the axis) and map them into the right axis’ data range. Then set the right ticks to those mapped values so they line up perfectly.

    Also fix this bug first:

    # was using ymin_right before defining it
    ymin_right = 0 if all_right.min() > 0 else all_right.min()
    
    fig.canvas.draw()  # left ticks are finalized
    
    # set right limits however you want first
    ax2.set_ylim(ymin_right, ymax_right)
    
    # map left major ticks -> right coords
    y0l, y1l = ax.get_ylim()
    y0r, y1r = ax2.get_ylim()
    left_major = ax.get_yticks()
    frac = (left_major - y0l) / (y1l - y0l)
    right_major = y0r + frac * (y1r - y0r)
    ax2.yaxis.set_major_locator(ticker.FixedLocator(right_major))
    
    # this is optional but you can mirror minor ticks too
    left_minor = ax.yaxis.get_minorticklocs()
    if len(left_minor):
        frac_m = (left_minor - y0l) / (y1l - y0l)
        right_minor = y0r + frac_m * (y1r - y0r)
        ax2.yaxis.set_minor_locator(ticker.FixedLocator(right_minor))
    

    Connect a tiny callback that recomputes the same mapping on ylim_changed if you want it to stay synced on zoom/limit changes.