Chaco: How to avoid zoom reset to reset axis range - python

I have set the range of my y-axis to a fixed range
plot.y_axis.mapper.range.set(low_setting=ylim[0], high_setting=ylim[1])
Then, when the user uses the zoom tool, and clicks the reset zoom key, e.g. ESC, the y-axis is reset to tightbounds. But my application has no idea that this has happened and is not able to set the axis limits correctly again. I am using the BetterSelectingZoom tool.
I see that in the _reset_range_settings method of BetterSelectingZoom, the range high and low setting is reset to BetterSelectingZoom._orig_low_setting, which is set to 'auto', but this overrides the setting I have set in the range. The _orig_low_setting is retrieved from the range when created, and not updated later when the zooming is actually done. So if you change the limits of your plot after the zoom tool creation, you will experience this issue. It seems like _reset_range_settings is called after the revert on the SelectZoomState, thus overriding the prev attribute in the zoom state. Is this a bug?
To make it work, I can set the _orig_low_setting attribute in the zoom tool, or override the BetterSelectingZoom _reset_range_settings method, but I feel bad messing around with private Traits
Code sample:
plot = Plot(self._plot_data, padding=10, border_visible=True)
...
plot.bgcolor = 'white'
vertical_grid = PlotGrid(component=plot,
mapper=plot.index_mapper,
orientation='vertical',
line_color="gray",
line_style='dot',
use_draw_order=True)
horizontal_grid = PlotGrid(component=plot,
mapper=plot.value_mapper,
orientation='horizontal',
line_color="gray",
line_style='dot',
use_draw_order=True)
vertical_axis = PlotAxis(orientation='left',
mapper=plot.value_mapper,
use_draw_order=True, tick_label_font=font)
horizontal_axis = PlotAxis(orientation='bottom',
mapper=plot.index_mapper,
use_draw_order=True, tick_label_font=font)
horizontal_axis.tick_generator = XTickGenerator()
vertical_axis.tick_generator = YTickGenerator()
plot.underlays.append(vertical_grid)
plot.underlays.append(horizontal_grid)
# Have to add axes to overlays because
# we are backbuffering the main plot,
# and only overlays get to render in addition to the backbuffer.
plot.overlays.append(vertical_axis)
plot.overlays.append(horizontal_axis)
# Enable Pan and Zoom
pan = PanTool(plot, restrict_to_data=True,
constrain=False, constrain_direction="x",
constrain_key=None)
zoom = BetterSelectingZoom(component=plot,
tool_mode="box", restrict_domain=True,
always_on=True, drag_button="right",
x_min_zoom_factor=1, y_min_zoom_factor=1)
plot.tools.append(pan)
plot.overlays.append(zoom)
To fix the issue I did this
class NoRangeResetZoom(BetterSelectingZoom):
def _reset_range_settings(self):
pass
And
zoom = NoRangeResetZoom(component=plot,
tool_mode="box", restrict_domain=True,
always_on=True, drag_button="right",
x_min_zoom_factor=1, y_min_zoom_factor=1)
plot.overlays.append(zoom)

Related

Understanding the interaction between mark_line point overlay and legend

I have found some unintuitive behavior in the interaction between the point property of mark_line and the appearance of the color legend for Altair/Vega-Lite. I ran into this when attempting to create a line with very large and mostly-transparent points in order to increase the area that would trigger the line's tooltip, but was unable to preserve a visible type=gradient legend.
The following code is an MRE for this problem, showing 6 cases: the use of [False, True, and a custom OverlayMarkDef] for the point property and the use of plain and customized color encoding.
import pandas as pd
import altair as alt
# create data
df = pd.DataFrame()
df['x_data'] = [0, 1, 2] * 3
df['y2'] = [0] * 3 + [1] * 3 + [2] * 3
# initialize
base = alt.Chart(df)
markdef = alt.OverlayMarkDef(size=1000, opacity=.001)
color_encode = alt.Color(shorthand='y2', legend=alt.Legend(title='custom legend', type='gradient'))
marks = [False, True, markdef]
encodes = ['y2', color_encode]
plots = []
for i, m in enumerate(marks):
for j, c in enumerate(encodes):
plot = base.mark_line(point=m).\
encode(x='x_data', y='y2', color=c, tooltip=['x_data','y2']).\
properties(title=', '.join([['False', 'True', 'markdef'][i], ['plain encoding', 'custom encoding'][j]]))
plots.append(plot)
combined = alt.vconcat(
alt.hconcat(*plots[:2]).resolve_scale(color='independent'),
alt.hconcat(*plots[2:4]).resolve_scale(color='independent'),
alt.hconcat(*plots[4:]).resolve_scale(color='independent')
).resolve_scale(color='independent')
The resulting plot (the interactive tooltips work as expected):
The color data is the same for each of these plots, and yet the color legend is all over the place. In my real case, the gradient is preferred (the data is quantitative and continuous).
With no point on the mark_line, the legend is correct.
Adding point=True converts the legend to a symbol type - I'm not sure why this is the case since the default legend type is gradient for quantitative data (as seen in the first row) and this is the same data - but can be forced back to gradient by the custom encoding.
Attempting to make a custom point via OverlayMarkDef however renders the forced gradient colorbar invisible - matching the opacity of the OverlayMarkDef. But it is not simply a matter of the legend always inheriting the properties of the point, because the symbol legend does not attempt to reflect the opacity.
I would like to have the normal gradient colorbar available for the custom OverlayMarkDef, but I would also love to build up some intuition for what is going on here.
The transparency issue with the bottom right plot has been fixed since Altair 4.2.0, so now all occasions that include a point on the line changes the legend to 'Ordinal' instead of 'Quantitative'.
I believe the reason the legend is converted to a symbol instead of a gradient, is that your are adding filled points and the fill channel is not set to a quantitative field so it defaults to either ordinal or nominal with a sort:
plot = base.mark_line().encode(
x='x_data',
y='y2',
color='y2',
)
plot + plot.mark_circle(opacity=1)
mark_point gives a gradient legend since it has not fill, and if we set the fill for mark_circle explicitly we also get a gradient legend (one for fill and one for color.
plot = base.mark_line().encode(
x='x_data',
y='y2',
color='y2',
fill='y2'
)
plot + plot.mark_circle(opacity=1)
I agree with you that this is a bit unexpected and it would be more convenient if the encoding type of point=True was set to the same as that used for the lines. You might suggest this as an enhancement in VegaLite together with reporting the apparent bug that you can't override the legend type via type='gradient'.

Holoviews DynamicMap Area or Curve with two streams is showing wrong chart

I would like to use HoloViews DynamicMap with a widget to select data for two curves, and a widget to control whether the curves are shown separately or as a filled area. It almost works, but sometimes shows the wrong data, depending on the order in which the widgets are manipulated.
The code snippet below demonstrates the issue, if run in a Jupyter notebook. It creates two identical DynamicMaps to show how they get out of sync with the widgets.
For this demo, if 'fill', an Area chart is shown. Otherwise, two Curve elements show the top and bottom bounds of the same area.
If 'higher', the area or curves are shifted upwards along the vertical axis (higher y values).
First, one DynamicMap is displayed. The code snippet then toggles the widget for 'fill' followed by 'higher', in that order (alternatively, the user could manually toggle the widgets). The DynamicMap should show a filled area in the higher position, but actually shows a filled area in the lower position. The image below the code snippet shows this incorrect DynamicMap on the left.
The second DynamicMap (shown on the right) is added to the display after the widgets are toggled. It correctly displays a chart corresponding to the state of the widgets at that point.
Code snippet
import holoviews as hv
import numpy as np
import panel as pn
pn.extension()
# Make two binary widgets to control whether chart
# data is high or low, and whether chart shows
# an area fill or just a pair of lines.
check_boxes = {name: pn.widgets.Checkbox(value=False, name=name) \
for name in ["higher", "fill"]}
# Data for charts.
xvals = [0.10, 0.90]
yvals_high = [1, 1.25]
yvals_low = [0.25, 0.40]
# Declare horizontal and vertical dimensions to go on charts.
xdim = hv.Dimension("x", range=(-0.5, 1.5), label="xdim")
ydim = hv.Dimension("y", range=(0, 2), label="ydim")
def make_plot(higher, fill):
"""Make high or low, filled area or line plot"""
yvals_line1 = np.array(yvals_high if higher else yvals_low)
yvals_line2 = 1.2*yvals_line1
if fill:
# Make filled area plot with x series and two y series.
area_data = (xvals, yvals_line1, yvals_line2)
plot = hv.Area(area_data,
kdims=xdim,
vdims=[ydim, ydim.clone("y.2")])
plot = hv.Overlay([plot]) # DMap will want an overlay.
else:
# Make line plot with x series and y series.
line_data_low = (xvals, yvals_line1)
line_data_high = (xvals, yvals_line2)
plot = hv.Curve(line_data_low,
kdims=xdim,
vdims=ydim) \
* hv.Curve(line_data_high,
kdims=xdim,
vdims=ydim)
return plot
# Map combinations of higher and fill to corresponding charts.
chart_dict = {(higher, fill): make_plot(higher, fill) \
for higher in [False,True] for fill in [False,True]}
def chart_func(higher, fill):
"""Return chart from chart_dict lookup"""
return chart_dict[higher, fill]
# Make two DynamicMaps linked to the check boxes.
dmap1 = hv.DynamicMap(chart_func, kdims=["higher", "fill"], streams=check_boxes)
dmap2 = hv.DynamicMap(chart_func, kdims=["higher", "fill"], streams=check_boxes)
# Show the check boxes, and one of the DMaps.
widget_row = pn.Row(*check_boxes.values(), width=150)
dmap_row = pn.Row(dmap1, align='start')
layout = pn.Column(widget_row,
dmap_row)
display(layout)
## Optionally use following line to launch a server, then toggle widgets.
#layout.show()
# Toggle 'fill' and then 'higher', in that order.
# Both DMaps should track widgets...
check_boxes["fill"].value = True
check_boxes["higher"].value = True
# Show the other DMap, which displays correctly given the current widgets.
dmap_row.append(dmap2)
# But first dmap (left) is now showing an area in wrong location.
Notebook display
Further widget toggles
The code snippet below can be run immediately afterwards in another cell. The resulting notebook display is shown in an image below the code snippet.
The code here toggles the widgets again, 'fill' and 'higher', in that order (alternatively, the user could manually toggle the widgets).
The left DynamicMap correctly displays a chart corresponding to the state of the widgets at that point, that is, two lines in the lower position.
The right DynamicMap incorrectly shows the two lines in the higher position.
# Toggle 'fill' and then 'higher' again, in that order.
# Both DMaps should track widgets...
check_boxes["fill"].value = False
check_boxes["higher"].value = False
# But now the second DMap shows lines in wrong location.
Am I just going about this the wrong way?
Thanks for the detailed, reproducible report!
After running your example, I noticed two things:
Switching from pn.extension to hv.extension at the start seems to fix the strange behavior that I also observing when using the panel extension. Could you confirm that things work as expected when using the holoviews extension?
I was wondering why your DynamicMaps work via chart_dict and chart_func when you can just use your make_plot callback in the DynamicMaps directly, without modification.
If you can confirm that the extension used changes the behavior, could you file an issue about this? Thanks!

Can blocking of matplotlib's mainloop be limited to a single window/figure?--i.e., fig.show(blocking=True)

I have a time history of images (2 + 1)D arrays that I take various slices of and examine using ipython and each view is a matplotlib figure.
I have a custom class that uses matplotlib widgets (specifically a Slider) to allow an interactive window to open and view the images frame by frame as selected by the Slider. The widget works fine, but uses the plt.show() command to block, which is also fine until I'm done with the widget.
In order for control to pass back to the ipython command line, I have to close all matplotlib figures--I would like to be able to only close the window associated with the widget. Is there some method to enable this functionality?
Something like fig.show(blocking=True) would be what I imagine I want, i.e. limit the blocking of the GUI mainloop to only look for plt.close() of that window, but that does not appear to be currently implemented.
#ImportanceOfBeingEarnest, thanks for the response. I've added the code I use for the viewer widget. To initialize the object, you just need to provide a 3D array of [frames (t), y, x] values. i.e.
randomData = np.random.rand((5,5,5))
class showFrames(object):
def __init__(self, timeData):
self.data = timeData # 3D array of t, y, x values
self.fig, self.ax = plt.subplots(1)
self.im = None
self.frameStr = None
self.start()
def start(self):
# initialize GUI
Tmin = self.data.min()
Tmax = self.data.max()
frameInit = self.data.shape[0] - 1
self.im = self.ax.imshow(self.data[frameInit])
self.im.set_clim(Tmin, Tmax)
self.fig.colorbar(self.im)
self.frameStr = self.ax.text(0.1, 0.1, '', transform=self.ax.transAxes, color='white')
axis_color = 'yellow'
# Add frame and radius slider for tweaking the parameters
frame_slider_ax = self.fig.add_axes([0.25, 0.05, 0.65, 0.03], axisbg=axis_color)
frame_slider = Slider(frame_slider_ax, 'Frame', 0, frameInit, valinit=frameInit)
frame_slider.on_changed(self.frame_slider_on_changed)
plt.show()
def frame_slider_on_changed(self, i):
self.im.set_data(self.data[int(i)])
self.frameStr.set_text(str(int(i)))
self.fig.canvas.draw_idle()
Your Slider instance is being garbage collected because you don't retain a reference to it.
From the Slider documentation:
For the slider to remain responsive you must maintain a reference to it.
In this case self.slider=Slider(...) instead of slider=Slider(...).

Add legend or background image to Igraph 0.6 (for python) plot

I plot a graph with python 2.7 by using Igraph 0.6 with the Cairo extention for plotting. All good but I would like to add a legend each time I plot.
If I could only add a background image to the plot that would be also fine, because I make a white image with the right size and with the legend already added there (with general sign explanation).
None of this I can do, nor I can find by googleing it. Maybe I'm just unable to get on the right side of Google or to find the right keyword in Igraph documentations.
gp = Graph(). It's global. Has vertex and edge sequences etc. There are some lists which contain further information about vertexes and edges (in ex.: self.gp_cities, self.road_kind) Here is how I plot:
def showitshort(self,event):
global gp
layout = gp.layout("kk")
color_dict = {"1": "red", "20": "blue"}
visual_style = {}
visual_style["vertex_size"] = 15
visual_style["vertex_color"] = ["yellow"]
visual_style["edge_color"] = [color_dict[elektro] for elektro in self.road_kind]
visual_style["vertex_label"] = self.gp_cities
visual_style["layout"] = layout
visual_style["bbox"] = (4000, 2500)
visual_style["margin"] = 100
visual_style["vertex_label_dist"] = 5
visual_style["vertex_shape"] = "triangle-up"
plot(gp,**visual_style)
The right link I think is enough. Please help a little and Thank you in advance!
The trick is that you can pass an existing Cairo surface into plot and it will simply plot the graph on that surface instead of creating a new one. So, basically, you need to construct a Cairo surface (say, an ImageSurface), draw your legend using standard Cairo calls onto that surface, then pass the surface to plot as follows:
plot(gp, target=my_surface, **visual_style)
As far as I know, plot() will not show the graph itself when invoked this way; it will simply return a Plot object. You can call the show() method of the Plot object to show it or call the save() method to save it into a PNG file.

Why is PatchCollectin overriding visibility?

I am using the matplotlib PatchCollection to hold a bunch of matplotlib.patches.Rectangles. But I want them to be invisible when first drawn (only turn visible when something else is clicked). This works fine when I was drawing the Rectangle's straight to the canvas with add_artist, but I want to change this to using a PatchCollection. For some reason, when I create the PatchCollection and add it with add_collection, they are all visible.
self.plotFigure = Figure()
self.plotAxes = self.plotFigure.add_subplot(111)
self.selectionPatches = []
for node in self.nodeList:
node.selectionRect = Rectangle((node.posX - node.radius*0.15 , node.posY - node.radius*0.15),
node.radius*0.3,
node.radius*0.3,
linewidth = 0,
facecolor = mpl.colors.ColorConverter.colors['k'],
zorder = z,
visible = False)
self.selectionPatches.append(node.selectionRect)
self.p3 = PatchCollection(self.selectionPatches, match_original=True)
self.plotAxes.add_collection(self.p3)
If I iterate through self.selectionPatches and print out each Rectangle's get_visible(), it returns false. But they are clearly visible when they get drawn. If anyone can help me see why this is happening, I would be very grateful.
When you create a PatchCollection it extracts a whole bunch of information from the objects you hand in (shape, location, styling(if you use match_original)), but does not keep the patch objects around for later reference (so it discards the per-patch visible). If you want all of the rectangles to be visible/invisible together you can do
self.p3 = PatchCollection(self.selectionPatches,
match_original=True,
visible=False)
other wise I think you will have to group them into the sets you want to appear together.
Look at the __init__ function of PatchCollection(here) and the rest of the cascade up through Collection and Artist.

Categories