Appropriately positioning annotations on stacked barplot - python

I have annotated each bar on my stacked barplot but can't seem to get the annotations to be equivalent to the bar's position.
This is the code I have:
for i in ax_mult.patches:
width,height=i.get_width(),i.get_height()
x,z =i.get_xy()
ax_mult.annotate(str(i.get_height()),(i.get_x()+.30*width,i.get_height()+.1*height))
This is what I am getting

I guess your main problem was that you placed the text in y direction effectively at 1.1 * i.get_height(), without considering the initial offset i.get_y().
Try this:
for i in ax_mult.patches:
ix,iy=i.get_x(),i.get_y() ## gives you the bottom left of each patch
width,height=i.get_width(),i.get_height() ## the width & height of each patch
## to place the annotation at the center (0.5, 0.5):
ax.annotate(str(height),(ix+0.5*width, iy+0.5*height),ha="center",va="center")
## alternatively via ax.text():
# ax.text(ix+.5*width,iy+.5*height,height,ha="center",va="center" )
Note that you may need to "play around" with good offsets, especially in y-direction. The ha="center",va="center" parameters align the text exactly at the chosen coordinate (both horizontally: ha and vertically: va), which comes in handy if you'd like to put the labels e.g. aligned below the top end of the patch:
ax.annotate(str(height),(ix+0.5*width, iy+1.0*height),ha="center",va="top")
Or just above the top end of the patch:
ax.annotate(str(height),(ix+0.5*width, iy+1.0*height),ha="center",va="bottom")

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!

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)

How to write annotation outside the drawing in data coords

My graphic goes from y=-1 to y=10
I want to write a small piece of text in an arbitrary position, say at x=2000, y=5:
ax.annotate('MgII', xy=(2000.0, 5.0), xycoords='data')
Now I want the same, but this time the piece of text must be outside the graphic, but in the exact position I mark in data coordinates:
ax.annotate('MgII', xy=(2000.0, 10.5), xycoords='data')
But it then disappears (remember my graphic goes from -1 to 10). There is plenty of space free on top of the graphic.
And, if I specify
xy=(2000.0, 9.999)
then the label appears nearly where I want it, only it is too close to the top border of the picture. I want it at y=10.5, specifically.
ax.annotate('MgII', xy=(2000.0, 10.5), xycoords='data', annotation_clip=False)
By default in data units the annotation is only drawn if it is in axes.
You might be better off using a blended transform:
trans = ax.get_xaxis_transform() # x in data untis, y in axes fraction
ann = ax.annotate('MgII', xy=(2000, 1.05 ), xycoords=trans)
I just had the same problem and found another,very simple solution. Option of the annotate method:
annotation_clip=False
https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.annotate.html

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.

Categories