Python and Remove annotation from figure - python

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()

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 does this Bokeh Column Data Source Work?

I am trying to get a better understanding about the column data source in Bokeh (for Python). I found this code, but I can't seem to find the documentation that explains some things I am looking for, For instance:
Where is the callback from the lasso_select tool? I want to see where the expected functionality is described.
How is the functionality of the lasso_select described in code? (What if I want to change it?)
What is happening to the column data source so that the circles outside the lasso-select region change appearance? (I want to know how I can use the column data source for more complex visualization than is shown by this demo. So I'd like to know what dictionary field is being manipulated, and how is it being manipulated. For example, is there a hidden "color" field or something like that, which isn't explicit in this code?)
What code causes the figure to be redrawn when a lasso_select action is made?
I have many more questions related to this and the CDSView, but I'll stop here for now.
from bokeh.io import output_file, show
from bokeh.layouts import gridplot
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure
output_file("brushing.html")
x = list(range(-20, 21))
y0 = [abs(xx) for xx in x]
y1 = [xx**2 for xx in x]
# create a column data source for the plots to share
source = ColumnDataSource(data=dict(x=x, y0=y0, y1=y1))
TOOLS = "box_select,lasso_select,help"
# create a new plot and add a renderer
left = figure(tools=TOOLS, plot_width=300, plot_height=300, title=None)
left.circle('x', 'y0', source=source)
# create another new plot and add a renderer
right = figure(tools=TOOLS, plot_width=300, plot_height=300, title=None)
right.circle('x', 'y1', source=source)
p = gridplot([[left, right]])
show(p)
This is related to my previous question, where the only answer was very narrow in explaining for that specific question. However, I am really interested in what's going on under the hood to give the results that are seen. It would help my understanding a lot more if I could know some of those details.
1) There is no callback. The tool is responsible for defining a selection geometry, and and then hit-testing baed on that geometry. The hit test results are store in a selection property of the data source. Glyph renderers draw glyphs based on the selection property of their data source. If two glyph renderers (even on different plots) share the same data source, they will both draw the same set of selected/nonselected as a result.
2) If you mean the appearance of the normal vs selected vs non-selected objects, how to configure that is described in the docs here:
https://docs.bokeh.org/en/latest/docs/user_guide/styling.html#selected-and-unselected-glyphs
There are also a few properties on the LassoTool object itself, that control, e.g. whether a selection should be made on every mousemove, or only on mouseup, and what the selection overlay looks like. All of these are recorded in the ReferenceGuide. If you are asking how to change the implementation, as with msot everything in Bokeh, the real work is not done in Python, it is done in the JavaScript library BokehJS. The implementation of the LassoTool is here:
https://github.com/bokeh/bokeh/blob/master/bokehjs/src/lib/models/tools/gestures/lasso_select_tool.ts
If you want something fundamentally different you would need to implement your own custom model, including its JavaScript component. There is an entire User's Guide section about building custom extensions:
https://docs.bokeh.org/en/latest/docs/user_guide/extensions.html
3) The Plot is configured with various Renderers, one of which can be a GlyphRenderer. The GlyphRenderer itself does not draw anything, but it configures various sub-glyphs that are used to draw in specific situations:
glyph draws "normal" versions of glyphs (i.e. when there is no selection on the data source)
selected_glyph draws "selected" versions of glyphs (i.e. the ones inside a lasso or box tool when a selection is active)
nonselected_glyph draws the "non-selected" versions of glyphs (i.e. the ones outside a lasso or box tool when a selection is active) By default the non selection glyph is just a copy of the "normal" glyph with the alpha value set very low.
hover_glyph draws the "hovered" versions of glyphs (i.e. when a hover tool has inspected them)
You configure the appearance in the different situation by configuring properties on the glyphs that are used in each situation. There are sensible defaults for them, but they can be updated as described in the first link of 2)
4) BokehJS has an internal signal/slots event system that is used (among other things) to request canvas redraws whenever various properties change.

Picking (pick_event) the correct artist on tkinter canvas?

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.

Using Matplotlib in Python, how to get a list of all drawn plt.scatter()?

When using matplotlib, you can easily access whatever curves you have drawn using plt.plot() by looking in the ax.lines list and remove n-th curve like so: del ax.lines[n].
Does the equivalent exist for plt.scatter()? After drawing several scatters the ax.lines is still empty so there must be another list somewhere.
For context, I am building a measurement interface using Qt Designer where the user might want to discard previous curves, so being able to choose via such a list which one is deleted (instead of clearing everything) is good.
Thanks in advance!
For such problems you may first just print out the return type of the object you deal with.
scatter = plt.scatter(x,y)
print(type(scatter))
This prints <class 'matplotlib.collections.PathCollection'>. From the name of the object we might deduce that the scatter is part of the axes' collections,
ax.collections
Indeed,
print(scatter)
print(plt.gca().collections[0])
print the same object.

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.

Categories