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.
Related
This is an open ended question. I used a tkinter program to display large sets of data in matplotlib, however I want to make it run faster. So what I thought about is to only update the data in the plot as the user guides their mouse along the x and y axis. Is there a way to override the matplotlib module such that I can render data as the mouse pans across the axis?
For example, I have the current view within y = [0,1], however data exists at y = 2. I can make an algorithm to only pass data within that range in order to pass to the data analytics algorithm for display. I just want to know what possible steps exists in order to dynamically render data in matplotlib instead of having it all there at once. Perhaps using the navigational tool, it can render when the mouse is released? The data is quite large.
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.
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.
I’ve been working on bokeh plots and I’m trying to plot a line graph taking values from a database. But the plot kind of traces back to the initial point and I don’t want that. I want a plot which starts at one point and stops at a certain point (and circle back). I’ve tried plotting it on other tools like SQLite browser and Excel and the plot seems ok which means I must be doing something wrong with the bokeh stuff and that the data points itself are not in error.
I’ve attached the images for reference and the line of code doing the line plot. Is there something I’ve missed?
>>> image = fig.line(“x”, “y”, color=color, source=something)
(Assume x and y are integer values and I’ve specified x and y ranges as DataRange1d(bounds=(0,None)))
Bokeh does not "auto-close" lines. You can see this is the case by looking at any number of examples in the docs and repository, but here is one in particular:
http://docs.bokeh.org/en/latest/docs/gallery/stocks.html
Bokeh's .line method will only "close up" if that is what is in the data (i.e., if the last point in the data is a repeat of the first point). I suggest you actually inspect the data values in source.data and I believe you will find this to be the case. Then the question is why is that the case and how to prevent it from doing that, but that is not really a Bokeh question.
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()