Picking (pick_event) the correct artist on tkinter canvas? - python

I am trying to use the pick_event in matplotlib within the tkinter framework. I have a problem with picking the right artist on the plot. I have multiple annotated artist on the plots with arrows connecting the annotated points, and I am trying to move the annotations around because they overlap and are not readable. I can move them in some cases with mouse_motion without any issues. The problem is sometimes clicking an artist generates event from an earlier moved artist and then the older artist starts moving and I can see in my pick_event function that indeed the older artist was the one that generated the last event. Sometimes 3 artists produce events on clicking sometimes more, irrespective of their positions on the graph. How can I make sure to only move the artist which is clicked? I know their is a hierarchy in pick_event. Can somebody show me a snippet where annotations can be picked correctly? I assume when annotations are close by there can be a confusion as to which one is clicked but if the annotations are far away from each other they should be distinguishable.
Any pointers are welcome.

Related

Python interactive plot in browser: drag and drop arbitrary elements

I have my backend in Python and I'm trying to create an interactive plot in the web browser, where the user can drag and drop elements across the plot. The plan is then to hook onto the 'drop' event with some JS that updates values in a form and submits the form, which would then cause the backend to update the plot.
So far I have been using mpld3, but I am not invested in it. I would be open to a solution using some other package like Plotly or Bokeh.
What I am looking for is quite similar to this mpld3 example, except that I want to allow the user to drag objects other than the points. Specifically, I want to create some vertical and horizontal lines and allow the user to drag these horizontally and vertically, as illustrated in this picture:
I have gotten so far as to adapt the mpld3 plugin to actually work with the latest version of d3.js, and I am able to drag the points (see gist). However, I am not able to adapt the plugin to make the vertical lines draggable like the points in the example.
My first idea was something like
lines = ax.vlines(xdata,min(ydata),ydata)
plugins.connect(fig, DragPlugin(lines))
vlines() returns a matplotlib.collections.LineCollection item, and I don't know how to access individual lines inside that. I tried simply modifying the type check inside the __init__ (if isinstance(object, mpl.lines.Line2D) to be more permissive, but that didn't work.
My next idea was to just draw the lines using ax.plot():
lines = []
for i in range(len(xdata)):
from_xy = (xdata[i],min(ydata))
to_xy = (xdata[i],ydata[i])
xs = from_xy[0],to_xy[0]
ys = from_xy[1],to_xy[1]
lines.append(ax.plot(xs,ys, 'k-')[0])
In this case the list lines contains matplotlib.lines.Line2D objects, which I thought would be easier to manipulate individually in mpld3 than a LineCollection. However, the Plugin seems to be written to deal with a the entire set of red points as a Line2D object (like a polygonal chain with invisible edges, if you see what I mean). This is what I conclude from the fact that the string 'pts' is used to find the mpld3 id of the points. Just passing a Line2D object to DragPlugin, however, does not work:
# don't use the suffix:
def __init__(self, object):
self.dict_ = {"type": "drag","id": utils.get_id(object)}
# after defining `lines` as above:
plugins.connect(fig, DragPlugin(lines[0]))
I get the following in the browser console:
Uncaught TypeError: obj.elements(...).data(...).style is not a function
Which gets raised at the following part of the JS:
obj.elements()
.data(obj.offsets)
.style("cursor", "default")
.call(drag);
But I have no idea what that JS is doing and it's not documented in the plugin.
Another thing I will need is to restrict the vertical lines to be only draggable horizontally, and conversely for the horizontal lines. But first I would like to get dragging the lines to work at all.

How to create a user created textbox on top of matplotlib figure?

BackGround
I'm making this software right plot data as boxplots and scatter plots. To make the graph more informative and clear I wanted the user to be able to drag-create a textbox on top of the figure if they wanted to add notes. This way when people share the figure it can be explained through these notes.
Current Approach and Problem
I have used annotate function to create a textbox in the upper right corner of the screen to tell the user how many data points have been used, but there are of few problems with this approach.
This is the code I used to create a text box to tell the user how many data pts there are.
self.axes.text(.98, .98, 'Number of Data Points: {}'.format(len(self.cleanedY)),
verticalalignment='top', horizontalalignment='right',
transform=self.axes.transAxes,
color='black', fontsize=9.5)
Extensive input that would have to be asked of the user. This includes the position coordinates, the actual text box, type of alignment.
The only way I know how to collect this info would be to create a Qdialog window prompting for all these inputs, which would be too cumbersome.
User doesn't have much control over being able to freely position the textbox and would have to know exact decimals to position the text box on the figure.
This is incredibly inefficient and inflexible. I want a way for the user to EASILY create these notes on the plot figure.
TLDR: Is there any way to develop a simple drag and create textbox option on a plot figure?
I'm not sure if this is what your looking for, but i found this similar example online. It doesn't allow for user to create a textbox, but adds annotations to the figure and allows you to move them around.
here's the link
http://scipy-cookbook.readthedocs.io/items/Matplotlib_Drag_n_Drop_Text_Example.html

Python, matplotlib, wxpython - Anyway to move an canvas from one frame to another?

So I don't really have any code to show as it's a very large script I have currently. Having said that I will try and add a simple example shortly.
Basically lets say I have two frames in wxpython, each containing a single panel with a matplotlib canvas. Lets call these canvases graph1 and graph2.
So I know how to plot directly into graph1, using standard methods.
I was wondering if there is any way I could change the location of graph1 into that of graph2, i.e. move the plot to the second frame (window).
Basically I am trying to implement a full screen option for the user, which upon clicking a graph will maximise it into the second frame. I am hoping I can do this just by changing the references rather than re-drawing etc. as I want it to keep the original state of the graph when it maximises.

Python and Remove annotation from figure

I am using matplotlib inside of a wxpython GUI. In short, I have plotted a bunch of data. I then click on a data point (using DataCursor found at:
Is there a matplotlib equivalent of MATLAB's datacursormode?
or
Pop up annotations on matplotlib within wxPython
The second link is my actual implementation. I refrense the datacursor class from other classes. What i'm wondering, how do i remove the annotation by clicking an event button? For instance, I have an event button that updates my scatter plot (by using plot_handle.set_data and not by clearing the figure). However, the annotation remains exactly where it was regardless if the point is there or not. How do i remove it?
Thanks!
Most matplotlib objects have a remove() function (I think it is inherited from Artist). Just call that on the object you want to remove.
Edit:
If you have
dc = DataCursor(...) # what ever aruguements you give it
# bunch of code
dc.annotation.remove()
del dc
plt.draw() # need to redraw to make remove visible
Python has no notion of 'private' attributes, so you can just reach inside you object and call remove on the annotation. See tutorial on classes for more details.
The down side of this is that now you have an object is a strange state. If you have any other references to it, they might behave badly. If you want to keep the DataCursor object around you can modify the annotation object using it's set_* functions or change it's visibility to temporarily hide it (doc)
You need to add it like an artist to delete it like an artist
arrow2 = matplotlib.text.Annotation("I love it",xy=(0.5,0.5),xycoords='data',arrowprops=dict(arrowstyle="-"))
ax2.add_artist(arrow2)
arrow2.remove()

Matplotlib in wxpython-plotting aborts

I'm using matplotlib in my wx.Frame to draw some arrows and text at a certain position. The number and position of the arrows is based on previously created data.
I do some calculations but basically it's like this:
arrow = primer
for each primer:
draw one arrow at position y - 0.05 to the previos arrow (primer_y - 0.05).
The x position of the arrow comes from the data, which is calculated and scaled (not so important know, it's just to now where the arrow should be at primer_x).
Everything works fine until I have many arrows to draw. E.g. in my code example the data contain 62 arrows, where 18 are drawn correctly, the next 3 are missing but the text is there and the rest is completly missing.
Does anyone knows what could be the problem? I tried allready to change the FigureSize but it's only stretch the arrows.
Here's a quick and dirty working example, data is included in the code:
http://pastebin.com/7mQmZm2c
Any help is highly appreciated!
Thanks in advance!
Stefanie
Stick
print((primer_x+0.06, primer_y))
inside the loop. You'll find that the arrows cease to be drawn when
primer_y becomes negative.
Don't draw the arrows with self.axes.annotate while labeling the
arrow with self.fig.text. The axis and the figure use different
coordinate systems. Changing self.fig.text to self.axes.text
will allow you to use the same coordinate system, which will make it
easier for you to position the text under the arrows.
There are far too many
hard-coded numbers ("magic numbers") in your code. It makes it very
hard to fix because when one number changes, the rest to do not change
in a logical way along with it. If you can use formulas to define
the relationship between some of those magic numbers, your life will
be a lot easier.
For example, if you change the for-loop to
for primer,primer_y in zip(data,np.linspace(0.95,0.0,len(data))):
instead of decrementing primer_y by a fixed amount with each pass through the loop, then primer_y will automatically space itself between 0.95 down to 0.0 no matter how many items there are in data.
If you find the arrows are squished (vertically), you can expand the
figure by changing the figsize on this line:
self.fig = matplotlib.figure.Figure(figsize=(20, 30), facecolor='white')

Categories