6

I am working on plot with two axes which supports picking the lines. I am using matplotlib and the the twinx() command. Unfortunately the pick event is only called for the artists in the top-most axes (see example below).

import matplotlib.pyplot as plt
import numpy as np

def onPick(event):
    print(event.artist.get_label())
    
def pick():
    fig=plt.figure(figsize=(5, 4), dpi=100, tight_layout=True)
    axis_1=fig.add_subplot(111)
    axis_2=axis_1.twinx()

    axis_1.set_ylim(0, 10)
    axis_2.set_ylim(0, 10)

    x=np.array([1,2,3,4])
    y_1=np.array([1,1,1,1])
    y_2=y_1+4
    
    axis_1.plot(x, y_1, picker=5, label='line_1')
    axis_2.plot(x, y_2, picker=5, label='line_2')
    
    fig.canvas.mpl_connect('pick_event', onPick)
    plt.show()

if __name__=='__main__':
    pick()

Is there a way to pick the lines from the axis below?

EGuy
  • 211
  • 1
  • 3
  • 10
  • 1
    Does this answer your question? [Matplotlib picker event on secondary y-axis](https://stackoverflow.com/questions/55565393/matplotlib-picker-event-on-secondary-y-axis) – JohanC Jul 05 '20 at 12:40
  • I know this posting. I am searching for a solution in general. Not related to twinx(). How to realize the pick event with two axes? – EGuy Jul 06 '20 at 18:16
  • Well, it is impossible – JohanC Jul 06 '20 at 18:53

2 Answers2

2

Impossible. ^^ I found a solution. I do not pick from the axes, I pick from the legend.
I think this is a good compromise.

import matplotlib.pyplot as plt
import numpy as np
from numpy.random import rand


def onpick(event):
        print(event.artist.get_label())


if __name__ == '__main__':
    t=np.linspace(1, 10, 100)
    y1, y2=1*t, 2*t
    y3, y4=3*t, 4*t

    fig, ax1=plt.subplots()
    ax2=ax1.twinx()
    ax2._get_lines.prop_cycler = ax1._get_lines.prop_cycler # Send Color cycle state to second axis.
    
    line1, = ax1.plot(t, y1, lw=2, label='1 HZ')
    line2, = ax1.plot(t, y2, lw=2, label='2 HZ')
    line3, = ax2.plot(t, y3, lw=2, label='3 HZ')
    line4, = ax2.plot(t, y4, lw=2, label='4 HZ')
    
    leg=ax1.legend(handles=[line1, line2, line3, line4], bbox_to_anchor=(0,1.02,1,0.2), loc="lower left", mode="expand", borderaxespad=0, ncol=3)
    for line in leg.get_lines(): line.set_picker(5)
    
    fig.canvas.mpl_connect('pick_event', onpick)
    plt.show()
EGuy
  • 211
  • 1
  • 3
  • 10
  • This does work. But for some reason if I overlay the bbox_to_anchor over the chart, instead of above it, then it does not work change to `bbox_to_anchor=(0,0.95,1,0.2)` and you will notice that the 2Hz one dos not trigger the event – tomatoeshift Jan 30 '22 at 11:11
  • Found a workaround. Insted of `ax1.legend()` I used `plt.legend()` – tomatoeshift Jan 30 '22 at 11:29
1

Not impossible! But instead of using pick_event you must handle the button_press_event and determine for yourself what was picked, by looking through each plotted line to find the MouseEvent. Each axes also needs its own annotation, or you'll have a hard time finding the correct annotation position.

Here's what I did, inspired by this answer:

from dataclasses import dataclass
from matplotlib.backend_bases import PickEvent
import matplotlib.pyplot as plt
import matplotlib

def on_button_press(event):
    if not event.inaxes:
        return # off canvas.

    all_picked = [dca for dca in dcAxes if dca.line.contains(event)[0]]
    if not all_picked:
        return # nothing was picked

    picked = all_picked[0] # take the first

    ind = picked.line.contains(event)[1]['ind']
    x_index = ind[0]

    x_val = picked.line.get_xdata()[x_index]
    y_val = picked.line.get_ydata()[x_index]

    annotation_visbility = picked.annotation.get_visible()
    if annotation_visbility and picked.annotation.xy==(x_val,y_val):
        picked.annotation.set_visible(False)
        fig.canvas.draw_idle()
    else:
        picked.annotation.xy = (x_val,y_val)

        text_label = f'{picked.line.get_label()}:({x_val},{y_val})'
        picked.annotation.set_text(text_label)

        picked.annotation.set_visible(True)
        fig.canvas.draw_idle()

 # create data to plot
x = []
y = []
y.append([])
y.append([])
for i in range(10):
    x.append(i)
    y[0].append(i)
    y[1].append(i*i)

# create plots, saving axes/line/annotation for lookup
@dataclass
class DataClassAxes:
    ax: plt.axes
    line: matplotlib.lines.Line2D
    annotation: matplotlib.text.Annotation

dcAxes: list[DataClassAxes] = []

for i in range(2):
    if i==0:
        fig, ax = plt.subplots()
        line, = ax.plot(x, y[i], 'o', picker=5, color='red', label='reds')
    else:
        ax = dcAxes[0].ax.twinx()
        line, = ax.plot(x, y[i], 'o', picker=5, color='blue', label='blues')

    annotation = ax.annotate(
        text='',
        xy=(0, 0),
        xytext=(15, 15), # distance from x, y
        textcoords='offset points',
        bbox={'boxstyle': 'round', 'fc': 'w'},
        arrowprops={'arrowstyle': '->'}
    )
    annotation.set_visible(False)

    dcAxes.append(DataClassAxes(ax, line, annotation))

fig.canvas.mpl_connect('button_press_event', on_button_press)

plt.show()
Mark
  • 374
  • 4
  • 9