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.
Related
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!
I've tried implementing everything I could find regarding removing or updating arrows/annotations/patches and nothing seems to work.
Here are the important parts of the code:
for i in xrange(lim):
if 'arrowprops' in kwargs:
kwap = kwargs.pop('arrowprops')
ap = {'patchA':text} # Ensure arrow is clipped by the text
ap.update(kwap) # Add arrowprops from kwargs
bboxes = get_bboxes(texts, r, (1, 1), ax) #Will give error if you didn't put arrows
for j, (bbox, text) in enumerate(zip(bboxes, texts)):
drawArrow = ax.annotate("", # Add an arrow from the text to the point
xy = (orig_xy[j]),
xytext=get_midpoint(bbox),
arrowprops=ap,
*args, **kwargs)
drawArrow.remove() #removed arrows otherwise old arrows remain
plt.draw()
This is from lines 524 to 645 from here https://github.com/Santosh-Gupta/adjustText/blob/master/adjustText/init.py
Each time it goes into the loop, the location of the arrow end points are different, but just draws a new arrow without removing the old arrow, so the images look like this
https://snag.gy/bDERmr.jpg
I saw that the .remove() is the proper way to remove an annotation, and it's not updated until the plot is drawn again so that's why I put plt.draw()
This is bit hard to debug/test/isolate because it's a python package that I am trying to alter. But perhaps there are some other work-arounds that I can try and implement.
I would like to set the legend on a self defined, custom position.
My final goal would be to get the settings of an already existing chart and use the same settings for a new chart.
I read in the docs it's possible to set the legend like this:
(http://python-pptx.readthedocs.io/en/latest/api/enum/XlLegendPosition.html#xllegendposition)
from pptx.enum.chart import XL_LEGEND_POSITION
chart.has_legend = True
chart.legend.position = XL_LEGEND_POSITION.CUSTOM
But I get a ValueError:
ValueError: CUSTOM (-4161) not a member of XL_LEGEND_POSITION enumeration
Did I miss anything or how can I set the legend on a custom position?
The .CUSTOM member of the XL_LEGEND_POSITION is a reporting member only (roughly like "read-only"). It is intended as the value of the Legend.position property when the legend has been manually adjusted (dragged and dropped with the mouse using the UI). Unlike the other members of that enumeration, it is not "assignable" and could not by itself of course set the position to where you wanted it.
Custom placement of the legend is not yet supported by the python-pptx API. If you wanted to do it you'd have to manipulate the underlying XML with low-level lxml calls. You'd need to understand the relevant XML schema and semantics to know what to do with that XML to produce the result you were after. This sort of thing is commonly called a "workaround function" in python-pptx and python-docx (they work very similarly being based on the same architecture). A Google search on "python-pptx" OR "python-docx" workaround function will find you some examples used for other purposes that you may find helpful if you decide to take that approach.
I couldn't find a fully formed answer to this, so I thought it would be worth posting the workaround that I used:
from pptx.oxml.xmlchemy import OxmlElement
def SubElement(parent, tagname, **kwargs):
element = OxmlElement(tagname)
element.attrib.update(kwargs)
parent.append(element)
return element
def manuallySetLegendPosition(
chart,
x,
y,
w,
h
):
## Inside layout, add manualLayout
L = chart.legend._element.get_or_add_layout()
mL = L.get_or_add_manualLayout()
## Add xMode and yMode and set vals to edge
xM = SubElement(mL, 'c:xMode', val="edge")
xY = SubElement(mL, 'c:yMode', val="edge")
## Add x, value is between -1 and 1 as a proportion of the chart width
## point of reference on the legend is its centre, not top left
xE = SubElement(mL, 'c:x', val=str(x))
## Add y, same concept as above
yE = SubElement(mL, 'c:y', val=str(y))
## Add w, legend height as a proportion of chart height
wE = SubElement(mL, 'c:w', val=str(w))
## Add h, same concept as above
hE = SubElement(mL, 'c:h', val=str(h))
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)
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.