Custom legend position with python-pptx - python

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

Related

how to make pg.PlotItem.removeItem() recognize PlotDataItems solely off name?

I have a function that adds PlotDataItem to a specific plot widget, however, if I try to use the removeItem function on the plot widget, it doesn't really do anything. I was seeking help on how I can make remove item work for this specific scenario? Any other tips you may recommend for optimization, readability, etc. are also greatly appreciated as I am still fairly new to PyQt and even Python itself. Thank you!
This function includes the removeItem() function.
def updateGraph(self):
"""Clears and updates graph to match the toggled checkboxes.
"""
# self.graphWidget.clear()
for checkboxNumber, checkbox in enumerate(
self.scenarioWidget.findChildren(QtWidgets.QCheckBox)
):
if checkbox.isChecked():
peak = self._model.get_peak(checkboxNumber)
duration = self._model.get_duration(checkboxNumber)
self.drawLine(
name=checkbox.objectName(),
peak=peak,
color=2 * checkboxNumber,
duration=duration,
)
else:
self.graphWidget.removeItem(pg.PlotDataItem(name=checkbox.objectName()))
# TODO: Allow for removal of individual pg.PlotDataItems via self.graphWidget.removeItem()
This function is where the PlotDataItems are added to the plot widget.
def drawLine(self, name, peak, color, duration=100.0):
"""Graphs sinusoidal wave off given 'peak' and 'duration' predictions to model epidemic spread.
Arguments:
name {string} -- Name of scenario/curve
peak {float} -- Predicted peak (%) of epidemic.
color {float} -- Color of line to graph.
Keyword Arguments:
duration {float} -- Predicted duration of epidemic (in days). (default: {100.0})
"""
X = np.arange(duration)
y = peak * np.sin((np.pi / duration) * X)
self.graphWidget.addItem(
pg.PlotDataItem(X, y, name=name, pen=pg.mkPen(width=3, color=color))
)
You're creating a new object with pg.PlotDataItem(name=checkbox.objectName()), so it will not be found as it's completely new.
Untested but should work:
for item in self.graphWidget.listDataItems():
if item.name() == checkbox.objectName():
self.graphWidget.removeItem(item)

How to hack this Bokeh HexTile plot to fix the coords, label placement and axes?

Below is Bokeh 1.4.0 code that tries to draw a HexTile map of the input dataframe, with axes, and tries to place labels on each hex.
I've been stuck on this for two days solid, reading bokeh doc, examples and github known issues, SO, Bokeh Discourse and Red Blob Games's superb tutorial on Hexagonal Grids, and trying code. (I'm less interested in raising Bokeh issues for the future, and far more interested in pragmatic workarounds to known limitations to just get my map code working today.) Plot is below, and code at bottom.
Here are the issues, in rough decreasing order of importance (it's impossible to separate the root-cause and tell which causes which, due to the way Bokeh handles glyphs. If I apply one scale factor or coord transform it fixes one set of issues, but breaks another, 'whack-a-mole' effect):
The label placement is obviously wrong, but I can't seem to hack up any variant of either (x,y) coords or (q,r) coords to work. (I tried combinations of figure(..., match_aspect=True)), I tried 1/sqrt(2) scaling the (x,y)-coords, I tried Hextile(... size, scale) params as per redblobgames, e.g. size = 1/sqrt(3) ~ 0.57735).
Bokeh forces the origin to be top left, and y-coords to increase as you go down, however the default axis labels show y or r as being negative. I found I still had to use p.text(q, -r, .... I suppose I have to manually patch the auto-supplied yaxis labels or TickFormatter to be positive.
I use np.mgrid to generate the coord grid, but I still seem to have to assign q-coords right-to-left: np.mgrid[0:8, (4+1):0:-1]. Still no matter what I do, the hexes are flipped L-to-R
(Note: empty '' counties are placeholders to get the desired shape, hence the boolean mask [counties!=''] on grid coords. This works fine and I want to leave it as-is)
The source (q,r) coords for the hexes are integers, and I use 'odd-r' offset coords (not axial or hexagonal coords). No matter what HexTile(..., size, scale) args I use, one or both dimensions in the plot is wrong or squashed. Or whether I include the 1/sqrt(2) factor in coord transform.
My +q-axis is east and my +r-axis should be 120° SSE
Ideally I'd like to have my origin at bottom-left (math plot style, not computer graphics). But Bokeh apparently doesn't support that, I can live without that. However defaulting the y-axis labels to negative, while requiring a mix of positive and negative coords, is confusing. Anyway, how to hack an automatic fix to that with minimum grief? (manual p.yrange = Range1d(?, ?)?)
Bokeh's approach to attaching (hex) glyphs to plots is a hard idiom to use. Ideally I simply want to reference (q,r)-coords everywhere for hexes, labels, axes. I never want to see (x,y)-coords appearing on axes, label coords, tick-marks, etc. but seems Bokeh won't allow you. I guess you have to manually hack the axes and ticks later. Also, the plot<->glyph interface doesn't allow you to expose a (q,r) <-> (x,y) coord transform function, certainly not a bidirectional one.
The default axes don't seem to have any accessors to automatically find their current extent/limits; p.yaxis.start/end are empty unless you specified them. The result from p.yaxis.major_tick_in,p.yaxis.major_tick_out is also wrong, for this plot it gives (2,6) for both x and y, seems to be clipping those to the interior multiples of 2(?). How to automatically get the axes' extent?
My current plot:
My code:
import pandas as pd
import numpy as np
from math import sqrt
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
from bokeh.models.glyphs import HexTile
from bokeh.io import show
# Data source is a list of county abbreviations, in (q,r) coords...
counties = np.array([
['TE','DY','AM','DN', ''],
['DL','FM','MN','AH', ''],
['SO','LM','CN','LH', ''],
['MO','RN','LD','WH','MH'],
['GA','OY','KE','D', ''],
['', 'CE','LS','WW', ''],
['LC','TA','KK','CW', ''],
['KY','CR','WF','WX', ''],
])
#counties = counties[::-1] # UNUSED: flip so origin is at bottom-left
# (q,r) Coordinate system is “odd/even-r” horizontal Offset coords
r, q = np.mgrid[0:8, (4+1):0:-1]
q = q[counties!='']
r = r[counties!='']
sqrt3 = sqrt(3)
# Try to transform odd-r (q,r) offset coords -> (x,y). Per Red Blob Games' tutorial.
x = q - (r//2) # this may be slightly dubious
y = r
counties_df = pd.DataFrame({'q': q, 'r': r, 'abbrev': counties[counties!=''], 'x': x, 'y': y })
counties_ds = ColumnDataSource(ColumnDataSource.from_df(counties_df)) # ({'q': q, 'r': r, 'abbrev': counties[counties != '']})
p = figure(tools='save,crosshair') # match_aspect=True?
glyph = HexTile(orientation='pointytop', q='x', r='y', size=0.76, fill_color='#f6f699', line_color='black') # q,r,size,scale=??!?!!? size=0.76 is an empirical hack.
p.add_glyph(counties_ds, glyph)
p.xaxis.minor_tick_line_color = None
p.yaxis.minor_tick_line_color = None
print(f'Axes: x={p.xaxis.major_tick_in}:{p.xaxis.major_tick_out} y={p.yaxis.major_tick_in}:{p.yaxis.major_tick_out}')
# Now can't manage to get the right coords for text labels
p.text(q, -r, text=["(%d, %d)" % (q,r) for (q, r) in zip(q, r)], text_baseline="middle", text_align="center")
# Ideally I ultimately want to fix this and plot `abbrev` column as the text label
show(p)
There is an axial_to_cartesian function that will just compute the hex centers for you. You can then attach the labels in a variety of orientations and anchoring from these.
Bokeh does not force the origin to be anywhere. There is one axial to cartesian mapping Bokeh uses, exactly what is given by axial_to_cartesian. The position of the Hex tiles (and hence the cartesian coordinates that the axes display) follows from this. If you want different ticks, Bokeh affords lots of control points over both tick location and tick labelling.
There is more than one convention for Axial coords. Bokeh picked the one that has the r-axis tile "up an to the left", i.e. the one explicitly shown here:
https://docs.bokeh.org/en/latest/docs/user_guide/plotting.html#hex-tiles
Bokeh expects up-and-to-the-left axial coords. You will need to convert whatever coordinate system you have to that. For "squishing" you will need to set match_aspect=True to ensure the "data space" aspect ratio matches the "pixel space" aspect ratio 1-1.
Alternatively, if you don't or can't use auto-ranging you will need to set the plot size carefully and also control the border sizes with min_border_left etc to make sure the borders are always big enough to accommodate any tick labels you have (so that the inner region will not be resized)
I don't really understand this question, but you have absolute control over what ticks visually appear, regardless of the underlying tick data. Besides the built-in formatters, there is FuncTickFormatter that lets you format ticks any way you want with a snippet of JS code. [1] (And you also have control of where ticks are located, if you want that.)
[1] Please note the CoffeeScript and from_py_func options are both deprecated and being removed in then next 2.0 release.
Again, you'll want to use axial_to_cartesian to position anything other then Hex tiles. No other glyphs in Bokeh understand axial coordinates (which is why we provide the conversion function).
You misunderstood what major_tick_in and major_tick_out are for. They are literally how far the ticks visually extend inside and outside the plot frame, in pixels.
Auto-ranging (with DataRange1d) is only computed in the browser, in JavaScript, which is why the start/end are not available on the "Python" side. If you need to know the start/end, you will need to explicitly set the start/end, yourself. Note, however that match_aspect=True only function with DataRange1d. If you explicitly set start/end manually, Bokeh will assume you know what you want, and will honor what you ask for, regardless of what it does to aspect.
Below are my solution and plot. Mainly per #bigreddot's advice, but there's still some coordinate hacking needed:
Expecting users to pass input coords as axial instead of offset coords is a major limitation. I work around this. There's no point in creating a offset_to_cartesian() because we need to negate r in two out of three places:
My input is even-r offset coords. I still need to manually apply the offset: q = q + (r+1)//2
I need to manually negate r in both the axial_to_cartesian() call and the datasource creation for the glyph. (But not in the text() call).
The call needs to be: axial_to_cartesian(q, -r, size=2/3, orientation='pointytop')
Need p = figure(match_aspect=True ...) to prevent squishing
I need to manually create my x,y axes to get the range right
Solution:
import pandas as pd
import numpy as np
from math import sqrt
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, Range1d
from bokeh.models.glyphs import HexTile
from bokeh.io import curdoc, show
from bokeh.util.hex import cartesian_to_axial, axial_to_cartesian
counties = np.array([
['DL','DY','AM','', ''],
['FM','TE','AH','DN', ''],
['SO','LM','CN','MN', ''],
['MO','RN','LD','MH','LH'],
['GA','OY','WH','D' ,'' ],
['' ,'CE','LS','KE','WW'],
['LC','TA','KK','CW','' ],
['KY','CR','WF','WX','' ]
])
counties = np.flip(counties, (0)) # Flip UD for bokeh
# (q,r) Coordinate system is “odd/even-r” horizontal Offset coords
r, q = np.mgrid[0:8, 0:(4+1)]
q = q[counties!='']
r = r[counties!='']
# Transform for odd-r offset coords; +r-axis goes up
q = q + (r+1)//2
#r = -r # cannot globally negate 'r', see comments
# Transform odd-r offset coords (q,r) -> (x,y)
x, y = axial_to_cartesian(q, -r, size=2/3, orientation='pointytop')
counties_df = pd.DataFrame({'q': q, 'r': -r, 'abbrev': counties[counties!=''], 'x': x, 'y': y })
counties_ds = ColumnDataSource(ColumnDataSource.from_df(counties_df)) # ({'q': q, 'r': r, 'abbrev': counties[counties != '']})
p = figure(match_aspect=True, tools='save,crosshair')
glyph = HexTile(orientation='pointytop', q='q', r='r', size=2/3, fill_color='#f6f699', line_color='black') # q,r,size,scale=??!?!!?
p.add_glyph(counties_ds, glyph)
p.x_range = Range1d(-2,6)
p.y_range = Range1d(-1,8)
p.xaxis.minor_tick_line_color = None
p.yaxis.minor_tick_line_color = None
p.text(x, y, text=["(%d, %d)" % (q,r) for (q, r) in zip(q, r)],
text_baseline="middle", text_align="center")
show(p)

Python - Plot Node Hierarchy using iGraph

I have a dataset with staff and their job roles, and each job role is assigned a code: 0 for top-management, 1 for middle-management, and 2 for general staff. I now want to plot these roles using a hierarchical graph, so that all code 0 staff are on the top, 1 in the middle, and 2 at the bottom. I've found the layout in iGraph to do this (see below), however don't know how to control which nodes appear where. Is there a parameter that I'm missing to control this? Any help would be appreciated.
CSV:
https://github.com/Laurie-Bamber/Enron_Corpus/blob/master/15Below_60Employees_1.csv
Role Codes:
https://github.com/Laurie-Bamber/Enron_Corpus/blob/master/Dict_role_code.csv
GML:
https://github.com/Laurie-Bamber/Enron_Corpus/blob/master/15Below_60Employees_1.gml
P.S. edges refer to emails between staff, not measures of hierarchy.
Code:
G = Graph.Read_GML('Test.gml')
visual_style['layout'] = G.layout_reingold_tilford()
plot(G, **visual_style)
I am proposing a solution with a slight modification to what you asked for. If you plot the levels vertically and the people at a role level horizontally, there are many people at one level so the labels run into each other. Instead, I am plotting the role levels horizontally and the individuals at a level are spread out vertically, leaving plenty of room to see the labels.
I do not think that there is a pre-built layout function that does what you are asking. However, it is not very hard to make your own layout. The essential part of doing that is to assign x-y coordinates where you want the nodes to be plotted. After that, you can just use the Layout function to convert the coordinates into a layout object.
My scheme for assigning x-y coordinates will be that the x coordinate will be the role level ( 1,2, or 3). I will just assign y-coordinates by making each node at a role level one higher than the previous node at that level. I use a small dictionary to keep track of what height comes next for each of the levels.
I will use the file names of the files that you provided and will assume that these files are in the current working directory.
import csv
from igraph import *
## Load graph
G = Graph.Read_GML('15Below_60Employees_1.gml')
## Load role levels
reader = csv.reader(open('Dict_role_code.csv'))
dx = dict(reader)
## Create a layout
height = { '1':0, '2':0, '3':0 }
COORD = []
for L in G.vs['label']:
height[dx[L]] = height[dx[L]] + 1
COORD.append((float(dx[L]), height[dx[L]]))
LO = Layout(COORD)
## Create the style
visual_style = {}
visual_style['vertex_size'] = 8
visual_style['vertex_frame_color'] = 'orange'
visual_style['layout'] = LO
visual_style['margin'] = 60
visual_style['edge_color'] = '#00000044'
plot(G, **visual_style)
I think that this provides you with a good starting place. You can tweak the placement from here.

How to set the format for *all* matplotlib polar axis angular labels to be in terms of pi and radians?

This is not a duplicate of this or this, as the answer there was not at all satisfactory to my problem, I don't want to deal with this per label. This is also not a duplicate of this as it doesn't deal with my specific problem.
I want to set the angular axis labels of polar plots, not one by one, but by a single time initialization method. This must be possible, as there appear to be ways to similar things with other axes types.
I knew how to do this before hand, but handn't seen the exact same question here and good solutions were also not found here. While I'm not sure if this is the best method, it is certainly better than setting the format per label!
So the solution I've found is using the FunctionFormatter. The definition is short, so i'll just paste it here.
class FuncFormatter(Formatter):
"""
Use a user-defined function for formatting.
The function should take in two inputs (a tick value ``x`` and a
position ``pos``), and return a string containing the corresponding
tick label.
"""
def __init__(self, func):
self.func = func
def __call__(self, x, pos=None):
"""
Return the value of the user defined function.
`x` and `pos` are passed through as-is.
"""
return self.func(x, pos)
This formatter class will allow us to create a function, pass it as an argument, and the output of that function will be the format of our plot angular labels.
You can then use PolarAxis.xaxis.set_major_formatter(formatter) to use your newly create formatter and only the angular axis labels will be changed. The same thing can be done with the yaxis attribute instead, and will cause the inner radial labels to change as well.
Here is what our function looks like that we will pass:
def radian_function(x, pos =None):
# the function formatter sends
rad_x = x/math.pi
return "{}π".format(str(rad_x if rad_x % 1 else int(rad_x)))
it uses standard python formatting strings as an output, getting rid of unnecessary decimals and appending the pi symbol to the end of the string to keep it in terms of pi.
The full program looks like this:
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import math
def radian_function(x, pos =None):
# the function formatter sends
rad_x = x/math.pi
return "{}π".format(str(rad_x if rad_x % 1 else int(rad_x)))
ax = plt.subplot(111, projection='polar')
ax.set_rmax(4)
ax.set_rticks([1, 2, 3, 4])
ax.grid(True)
ax.set_title("Polar axis label example", va='bottom')
# sets the formatter for the entire set of angular axis labels
ax.xaxis.set_major_formatter(ticker.FuncFormatter(radian_function))
# sets the formatter for the radius inner labels.
#ax.yaxis.set_major_formatter(ticker.FuncFormatter(radian_function))
plt.show()
which outputs
You could further improve the formatter to check for one (so that 1π is simply shown as π) or check for 0 in a similar fashion. You can even use the position variable (which I left out since it was unnecessary) to further improve visual formatting.
such a function might look like this:
def radian_function(x, pos =None):
# the function formatter sends
rad_x = x/math.pi
if rad_x == 0:
return "0"
elif rad_x == 1:
return "π"
return "{}π".format(str(rad_x if rad_x % 1 else int(rad_x)))

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