I have a DataFrame with shape (14403, 438) that consists of longitudes and latitudes as well as values. The DataFrame is as:
I am plotting the coordinates as:
# define map colors
land_color = '#f5f5f3'
water_color = '#cdd2d4'
coastline_color = '#f5f5f3'
border_color = '#bbbbbb'
meridian_color = '#f5f5f3'
marker_fill_color = '#0000ff'
marker_edge_color = 'None'
# create the plot
fig = plt.figure(figsize = (15, 10))
ax = fig.add_subplot(111, facecolor = '#ffffff', frame_on = False)
ax.set_title('Transportable Array', fontsize = 24, color = '#333333')
#lon_0 center of desired map domain (in degrees).
#lat_0 center of desired map domain (in degrees).
#width width of desired map domain in projection coordinates (meters).
#height height of desired map domain in projection coordinates (meters).
# draw the basemap and its features
m = Basemap(width = 5500000,height = 3300000,
resolution = 'l', area_thresh = 1000., projection = 'lcc',\
lat_1 = 45., lat_2 = 55, lat_0 = 37, lon_0 = -98.)
m.drawmapboundary(color = border_color, fill_color = water_color)
m.drawcoastlines(color = coastline_color)
m.drawcountries(color = border_color)
m.fillcontinents(color = land_color, lake_color = water_color)
m.drawparallels(np.arange(-90., 120., 30.), color = meridian_color)
m.drawmeridians(np.arange(0., 420., 60.), color = meridian_color)
# project the location history points then scatter plot them
x, y = m(stations.loc['longitude'].values, stations.loc['latitude'].values)
m.scatter(x, y, s = 8, color = marker_fill_color, edgecolor = marker_edge_color, alpha = 1, zorder = 3)
# show & save the map
plt.savefig('Transportable_Array.png', dpi = 96, bbox_inches = 'tight', pad_inches = 0.2)
plt.show()
I am trying to create an animation that will plot the coordinates for each column and then iterate over the values in the index. In the end I am trying to have it iterate over the 14,403 rows and change the markings color based on the value. I am currently having trouble even animating the plot for the coordinates alone.
I would love to be able to implement bqplot, but the scatter animations I've followed on GitHub have not worked yet.
The map currently looks like below. It'd be wicked cool if each dot can fluctuate in color based on the current iterations value.
Thank you for reading.
You can use the animation module for this. These are the general steps:
Convert the values into a colour
Update the color at each step
Save the animation
Here is some code:
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
import matplotlib.animation as animation
import numpy as np
land_color = '#f5f5f3'
water_color = '#cdd2d4'
coastline_color = '#f5f5f3'
border_color = '#bbbbbb'
meridian_color = '#f5f5f3'
marker_fill_color = '#0000ff'
marker_edge_color = 'None'
# Some dummy data
longVals = np.random.uniform(-120,-80, 1000)
latVals = np.random.uniform(35, 45, 1000)
vals = np.random.uniform(size=(200,1000))
# Be careful - the values that go into the colormap function
# must be integers between 0 and 254
normalisedVals = 254*(vals-vals.min())/(vals.max()-vals.min())
normalisedVals = normalisedVals.astype(np.int)
cm = plt.cm.spectral_r
fig = plt.figure(figsize = (15, 10))
ax = fig.add_subplot(111, facecolor = '#ffffff', frame_on = False)
ax.set_title('Transportable Array', fontsize = 24, color = '#333333')
# draw the basemap and its features
m = Basemap(width = 5500000,height = 3300000,
resolution = 'l', area_thresh = 1000., projection = 'lcc',
lat_1 = 45., lat_2 = 55, lat_0 = 37, lon_0 = -98.)
m.drawmapboundary(color = border_color, fill_color = water_color)
m.drawcoastlines(color = coastline_color)
m.drawcountries(color = border_color)
m.fillcontinents(color = land_color, lake_color = water_color)
m.drawparallels(np.arange(-90., 120., 30.), color = meridian_color)
m.drawmeridians(np.arange(0., 420., 60.), color = meridian_color)
x, y = m(longVals, latVals)
scat = m.scatter(x, y, s = 8, c = normalisedVals[0], edgecolor = marker_edge_color, alpha = 1, zorder = 3)
def init():
return scat,
def animate(i):
col = cm(normalisedVals[i])
scat.set_color(col)
return scat,
anim = animation.FuncAnimation(fig, animate, init_func=init, frames=100, interval=20, blit=False, repeat=False)
anim.save('animation.gif', writer='imagemagick', fps=60)
I should warn you that for 14k rows this will take a while.
Also I would recommend saving as an mp4 rather than a gif due to better compression.
If you have any questions let me know!
Related
I have got a matplotlib question about xticks. I wanted to hide all those values that do not occur. I actually did it, but for the second set of values (red chart). I found how to hide for a specific data frame but not for 2 or more.
This is my code:
plt.subplots(figsize=(2, 1), dpi=400)
width = 0.005
xlim = np.arange(0, 1, 0.01)
ylim = np.arange(0, 0.1, 0.001)
plt.xticks(density_2.index.unique(), rotation=90, fontsize=1.5)
plt.yticks(density_2.unique(), fontsize=2)
plt.bar(density_1.index, density_1, width, color='Green', label=condition_1,alpha=0.5)
plt.bar(density_2.index, density_2, width, color='Red', label=condition_2,alpha=0.5)
plt.legend(loc="upper right", fontsize=2)
plt.show()
Link where I saw the solution: show dates in xticks only where value exist in plot chart and hide unnecessary interpolated xtick labels
Thank you very much in advance!
You need to find the intersection of the two lists of density_1's and density_2's ticks, as reported here.
Working example:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
N = 150
values_1 = np.random.randint(low = 5, high = 75, size = N)/100
density_1 = pd.DataFrame({'density_1': values_1})
density_1 = density_1.value_counts().sort_index(ascending = True)
density_1.index = sorted(list(set(values_1)), reverse = False)
values_2 = np.random.randint(low = 35, high = 100, size = N)/100
density_2 = pd.DataFrame({'density_2': values_2})
density_2 = density_2.value_counts().sort_index(ascending = True)
density_2.index = sorted(list(set(values_2)), reverse = False)
width = 0.005
condition_1 = 'Adele'
condition_2 = 'Extremoduro'
fig, ax = plt.subplots(figsize = (10, 5))
ax.bar(density_1.index, density_1, width, color = 'Green', label = condition_1, alpha = 0.5)
ax.bar(density_2.index, density_2, width, color = 'Red', label = condition_2, alpha = 0.5)
ax.legend(loc = 'upper right')
ax.set_xticks(list(set(density_1.index.unique()) & set(density_2.index.unique())), rotation = 90)
plt.show()
In the line:
list(set(density_1.index.unique()) & set(density_2.index.unique()))
you can select ticks which blongs to both density_1 and density_2.
Zoom in:
I want to generate a plot like the below:
At the moment I am trying to play around with the alpha parameter:
import numpy as np
from matplotlib import pyplot as plt
xlocations_edensity = np.loadtxt("edensity_xaxis.txt")
ylocations_edensity = np.loadtxt("edensity_yaxis.txt")
xlocsedensity, ylocsedensity = np.meshgrid(xlocations_edensity, ylocations_edensity)
xlocations_Efield = np.loadtxt("Efield_x_axis.txt")
ylocations_Efield = np.loadtxt("Efield_y_axis.txt")
xlocsEfield, ylocsEfield = np.meshgrid(xlocations_Efield, ylocations_Efield)
edensitytensor = np.load("edensitytensor.npy") # shape (76, 257, 65)
Efieldtensor = np.load("Efieldtensor.npy")
fig, ax = plt.subplots()
ax.set(xlabel="x position [um]", ylabel="y position [um] \n")
pos2 = ax.pcolor(xlocations_Efield, ylocations_Efield, Efieldtensor[40, :, :].T, cmap="Reds", alpha=0.9)
fig.colorbar(pos2, ax=ax, label="\n Efield value [MV/m]")
pos1 = ax.pcolor(xlocations_edensity, ylocations_edensity, edensitytensor[100, :, :].T, cmap="Blues", alpha=0.5)
fig.colorbar(pos1, ax=ax, label="\n electron density value [cm^(-3)]")
plt.savefig("Efield_edensity_map.pdf")
But changing the order of plotting, I get different results. One color map ''hides'' the other.
Say I plot the Reds one first, it appears and the Blues one is hidden.
The other way around, Blues first and Reds first, the Blues hides the Reds.
The result of the above code is:
Do you have anything in mind what shall I do?
Thank you!
Setting the alpha value of the pcolor call is not that good because it applies the same transparency to all the colors on the colormap.
You could use a custom colormap with an evolving transparency, I present my try with linear and sigmoidal evolutions of alpha, you could try others. I created dummy noisy data with a Gaussian pulse to simulate the data as in your example.
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.colors import ListedColormap
# generating dummy data
nx, ny = 257, 65
x_field, y_field = np.linspace(0,10,nx), np.linspace(0,6,ny)
field = np.random.rand(nx,ny)
# normalizing
field -= np.min(field); field /= np.max(field)
x_density, y_density = np.linspace(1,6,nx), np.linspace(1,6,ny)
Y, X = np.meshgrid(y_density,x_density,)
density = np.random.rand(nx,ny) # shape (76, 257, 65)
gaussian_center = (4.0,4.0)
distance_square = (X - gaussian_center[0])**2 + (Y - gaussian_center[1])**2
density += 5.0*np.exp(-distance_square/4.0)
# normalizing
density -= np.min(density); density /= np.max(density)
# getting the original colormap
orig_cmap = plt.get_cmap('Blues')
cmap_n = orig_cmap.N
derived_cmap = orig_cmap(np.arange(cmap_n))
fig, axs = plt.subplots(
4,3,
gridspec_kw={"width_ratios":[1, 0.025, 0.025]},
figsize=(10,8),
constrained_layout=True)
# original
row_subplot = 0
ax = axs[row_subplot,0]
ax.set_ylabel("original")
image_field = ax.pcolor(
x_field, y_field, field.T,
cmap="Reds", shading='auto')
fig.colorbar(image_field, cax=axs[row_subplot,-2],)
image_density = ax.pcolor(
x_density, y_density, density.T,
cmap=orig_cmap, shading="auto")
fig.colorbar(image_density, cax=axs[row_subplot,-1],)
# option 1 - transparent pseudocolor for the above image
row_subplot = 1
ax = axs[row_subplot,0]
ax.set_ylabel("transparent pcolor")
image_field = ax.pcolor(
x_field, y_field, field.T,
alpha=1.0, cmap="Reds", shading='auto')
fig.colorbar(image_field, cax=axs[row_subplot,-2],)
image_density = ax.pcolor(
x_density, y_density, density.T,
alpha=0.5, cmap=orig_cmap, shading="auto")
fig.colorbar(image_density, cax=axs[row_subplot,-1],)
# option 2 - linear gradient colormap
linear_cmap = derived_cmap.copy()
linear_cmap[:,-1] = np.arange(cmap_n)/cmap_n
linear_cmap = ListedColormap(linear_cmap)
row_subplot = 2
ax = axs[row_subplot,0]
ax.set_ylabel("linear gradient")
image_field = ax.pcolor(
x_field, y_field, field.T,
cmap="Reds", shading='auto')
fig.colorbar(image_field, cax=axs[row_subplot,-2],)
image_density = ax.pcolor(
x_density, y_density, density.T,
cmap=linear_cmap, shading="auto")
fig.colorbar(image_density, cax=axs[row_subplot,-1],)
# option 3 - sigmoid gradient
sigmoid_cmap = derived_cmap.copy()
x = np.linspace(-10,10,cmap_n)
sigmoid_cmap[:,-1] = np.exp(x)/(np.exp(x) + 1)
sigmoid_cmap = ListedColormap(sigmoid_cmap)
row_subplot = 3
ax = axs[row_subplot,0]
ax.set_ylabel("sigmoid gradient")
image_field = ax.pcolor(
x_field, y_field, field.T,
cmap="Reds", shading='auto')
fig.colorbar(image_field, cax=axs[row_subplot,-2],)
image_density = ax.pcolor(
x_density, y_density, density.T,
cmap=sigmoid_cmap, shading="auto")
fig.colorbar(image_density, cax=axs[row_subplot,-1],)
Background
In Matplotlib, we can render the string using mathtext as a marker using $ ..... $ (Reference 1)
Question
Is there any way to enclose this text in a circular or rectangular box, or any different different shape? Similar to the registered symbol as shown here
I want to use this marker on a plot as shown below:
Text '$T$' is used in this plot, I want the text to be enclosed in a circle or rectangle.
Solution
As suggested in the comments of the answer, I have plotted a square marker of a bit larger size before the text marker. This resolved the issue.
The final figure is shown below:
Edit: Easiest way is to simply place patches to be the desired "frames" in the same location as the markers. Just make sure they have a lower zorder so that they don't cover the data points.
More sophisticated ways below:
You can make patches. Here is an example I used to make a custom question mark:
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.markers as m
fig, ax = plt.subplots()
lim = -5.8, 5.7
ax.set(xlim = lim, ylim = lim)
marker_obj = m.MarkerStyle('$?$') #Here you place your letter
path = marker_obj.get_path().transformed(marker_obj.get_transform())
path._vertices = np.array(path._vertices)*8 #To make it larger
patch = mpl.patches.PathPatch(path, facecolor="cornflowerblue", lw=2)
ax.add_patch(patch)
def translate_verts(patch, i=0, j=0, z=None):
patch._path._vertices = patch._path._vertices + [i, j]
def rescale_verts(patch, factor = 1):
patch._path._vertices = patch._path._vertices * factor
#translate_verts(patch, i=-0.7, j=-0.1)
circ = mpl.patches.Arc([0,0], 11, 11,
angle=0.0, theta1=0.0, theta2=360.0,
lw=10, facecolor = "cornflowerblue",
edgecolor = "black")
ax.add_patch(circ)#One of the rings around the questionmark
circ = mpl.patches.Arc([0,0], 10.5, 10.5,
angle=0.0, theta1=0.0, theta2=360.0,
lw=10, edgecolor = "cornflowerblue")
ax.add_patch(circ)#Another one of the rings around the question mark
circ = mpl.patches.Arc([0,0], 10, 10,
angle=0.0, theta1=0.0, theta2=360.0,
lw=10, edgecolor = "black")
ax.add_patch(circ)
if __name__ == "__main__":
ax.axis("off")
ax.set_position([0, 0, 1, 1])
fig.canvas.draw()
#plt.savefig("question.png", dpi=40)
plt.show()
Edit, second answer:
creating a custom patch made of other patches:
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import mpl_toolkits.mplot3d.art3d as art3d
class PlanetPatch(mpl.patches.Circle):
"""
This class combines many patches to make a custom patch
The best way to reproduce such a thing is to read the
source code for all patches you plan on combining.
Also make use of ratios as often as possible to maintain
proportionality between patches of different sizes"""
cz = 0
def __init__(self, xy, radius,
color = None, linewidth = 20,
edgecolor = "black", ringcolor = "white",
*args, **kwargs):
ratio = radius/6
mpl.patches.Circle.__init__(self, xy, radius,
linewidth = linewidth*ratio,
color = color,
zorder = PlanetPatch.cz,
*args, **kwargs)
self.set_edgecolor(edgecolor)
xy_ringcontour = np.array(xy)+[0, radius*-0.2/6]
self.xy_ringcontour = xy_ringcontour - np.array(xy)
self.ring_contour = mpl.patches.Arc(xy_ringcontour,
15*radius/6, 4*radius/6,
angle =10, theta1 = 165,
theta2 = 14.5,
fill = False,
linewidth = 65*linewidth*ratio/20,
zorder = 1+PlanetPatch.cz)
self.ring_inner = mpl.patches.Arc(xy_ringcontour,
15*radius/6, 4*radius/6,
angle = 10, theta1 = 165 ,
theta2 = 14.5,fill = False,
linewidth = 36*linewidth*ratio/20,
zorder = 2+PlanetPatch.cz)
self.top = mpl.patches.Wedge([0,0], radius, theta1 = 8,
theta2 = 192,
zorder=3+PlanetPatch.cz)
self.xy_init = xy
self.top._path._vertices=self.top._path._vertices+xy
self.ring_contour._edgecolor = self._edgecolor
self.ring_inner.set_edgecolor(ringcolor)
self.top._facecolor = self._facecolor
def add_to_ax(self, ax):
ax.add_patch(self)
ax.add_patch(self.ring_contour)
ax.add_patch(self.ring_inner)
ax.add_patch(self.top)
def translate(self, dx, dy):
self._center = self.center + [dx,dy]
self.ring_inner._center = self.ring_inner._center +[dx, dy]
self.ring_contour._center = self.ring_contour._center + [dx,dy]
self.top._path._vertices = self.top._path._vertices + [dx,dy]
def set_xy(self, new_xy):
"""As you can see all patches have different ways
to have their positions updated"""
new_xy = np.array(new_xy)
self._center = new_xy
self.ring_inner._center = self.xy_ringcontour + new_xy
self.ring_contour._center = self.xy_ringcontour + new_xy
self.top._path._vertices += new_xy - self.xy_init
fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot()
lim = -8.5, 8.6
ax.set(xlim = lim, ylim = lim,
facecolor = "black")
planets = []
colors = mpl.colors.cnames
colors = [c for c in colors]
for x in range(100):
xy = np.random.randint(-7, 7, 2)
r = np.random.randint(1, 15)/30
color = np.random.choice(colors)
planet = PlanetPatch(xy, r, linewidth = 20,
color = color,
ringcolor = np.random.choice(colors),
edgecolor = np.random.choice(colors))
planet.add_to_ax(ax)
planets.append(planet)
fig.canvas.draw()
#plt.savefig("planet.png", dpi=10)
plt.show()
I am plotting changes in mean and variance of some data with the following code
import matplotlib.pyplot as pyplot
import numpy
vis_mv(data, ax = None):
if ax is None: ax = pyplot.gca()
cmap = pyplot.get_cmap()
colors = cmap(numpy.linspace(0, 1, len(data)))
xs = numpy.arange(len(data)) + 1
means = numpy.array([ numpy.mean(x) for x in data ])
varis = numpy.array([ numpy.var(x) for x in data ])
vlim = max(1, numpy.amax(varis))
# variance
ax.imshow([[0.,1.],[0.,1.]],
cmap = cmap, interpolation = 'bicubic',
extent = (1, len(data), -vlim, vlim), aspect = 'auto'
)
ax.fill_between(xs, -vlim, -varis, color = 'white')
ax.fill_between(xs, varis, vlim, color = 'white')
# mean
ax.plot(xs, means, color = 'white', zorder = 1)
ax.scatter(xs, means, color = colors, edgecolor = 'white', zorder = 2)
return ax
This works perfectly fine:
but now I would like to be able to use this visualisation also in a vertical fashion as some kind of advanced color bar kind of thingy next to another plot. I hoped it would be possible to rotate the entire axis with all of its contents,
but I could only find this question, which does not really have a solid answer yet either. Therefore, I tried to do it myself as follows:
from matplotlib.transforms import Affine2D
ax = vis_mv()
r = Affine2D().rotate_deg(90) + ax.transData
for x in ax.images + ax.lines + ax.collections:
x.set_transform(r)
old = ax.axis()
ax.axis(old[2:4] + old[0:2])
This almost does the trick (note how the scattered points, which used to lie along the white line, are blown up and not rotated as expected).
Unfortunately the PathCollection holding the result of the scattering does not act as expected. After trying out some things, I found that scatter has some kind of offset transform, which seems to be the equivalent of the regular transform in other collections.
x = numpy.arange(5)
ax = pyplot.gca()
p0, = ax.plot(x)
p1 = ax.scatter(x,x)
ax.transData == p0.get_transform() # True
ax.transData == p1.get_offset_transform() # True
It seems like I might want to change the offset transform instead for the scatter plot, but I did not manage to find any method that allows me to change that transform on a PathCollection. Also, it would make it a lot more inconvenient to do what I actually want to do.
Would anyone know if there exists a possibility to change the offset transform?
Thanks in advance
Unfortunately the PathCollection does not have a .set_offset_transform() method, but one can access the private _transOffset attribute and set the rotating transformation to it.
import matplotlib.pyplot as plt
from matplotlib.transforms import Affine2D
from matplotlib.collections import PathCollection
import numpy as np; np.random.seed(3)
def vis_mv(data, ax = None):
if ax is None: ax = plt.gca()
cmap = plt.get_cmap()
colors = cmap(np.linspace(0, 1, len(data)))
xs = np.arange(len(data)) + 1
means = np.array([ np.mean(x) for x in data ])
varis = np.array([ np.var(x) for x in data ])
vlim = max(1, np.amax(varis))
# variance
ax.imshow([[0.,1.],[0.,1.]],
cmap = cmap, interpolation = 'bicubic',
extent = (1, len(data), -vlim, vlim), aspect = 'auto' )
ax.fill_between(xs, -vlim, -varis, color = 'white')
ax.fill_between(xs, varis, vlim, color = 'white')
# mean
ax.plot(xs, means, color = 'white', zorder = 1)
ax.scatter(xs, means, color = colors, edgecolor = 'white', zorder = 2)
return ax
data = np.random.normal(size=(9, 9))
ax = vis_mv(data)
r = Affine2D().rotate_deg(90)
for x in ax.images + ax.lines + ax.collections:
trans = x.get_transform()
x.set_transform(r+trans)
if isinstance(x, PathCollection):
transoff = x.get_offset_transform()
x._transOffset = r+transoff
old = ax.axis()
ax.axis(old[2:4] + old[0:2])
plt.show()
I'm struggling to adjust my plot legend after adding the axline/ hline on 100 level in the graph.(screenshot added)
if there's a way to run this correctly so no information will be lost in legend, and maybe add another hline and adding it to the legend.
adding the code here, maybe i'm not writing it properly.
fig, ax1 = plt.subplots(figsize = (9,6),sharex=True)
BundleFc_Outcome['Spend'].plot(kind = 'bar',color = 'blue',width = 0.4, ax = ax1,position = 1)
#
# Make the y-axis label, ticks and tick labels match the line color.
ax1.set_ylabel('SPEND', color='b', size = 18)
ax1.set_xlabel('Bundle FC',color='w',size = 18)
ax2 = ax1.twinx()
ax2.set_ylabel('ROAS', color='r',size = 18)
ax1.tick_params(axis='x', colors='w',size = 20)
ax2.tick_params(axis = 'y', colors='w',size = 20)
ax1.tick_params(axis = 'y', colors='w',size = 20)
#ax1.text()
#
ax2.axhline(100)
BundleFc_Outcome['ROAS'].plot(kind = 'bar',color = 'red',width = 0.4, ax = ax2,position = 0.25)
plt.grid()
#ax2.set_ylim(0, 4000)
ax2.set_ylim(0,300)
plt.title('ROAS & SPEND By Bundle FC',color = 'w',size= 20)
plt.legend([ax2,ax1],labels = ['SPEND','ROAS'],loc = 0)
The code gives me the following picture:
After implementing the suggestion in the comments, the picture looks like this (does not solve the problem):
You can use bbox_to_anchor attribute to set legend location manually.
ax1.legend([ax1],labels = ['SPEND'],loc='upper right', bbox_to_anchor=(1.25,0.70))
plt.legend([ax2,ax1],labels = ['SPEND','ROAS'],loc='upper right', bbox_to_anchor=(1.25,0.70))
https://matplotlib.org/users/legend_guide.html#legend-location
So finally figured it out , was simpler for a some reason
Even managed to add another threshold at level 2 for minimum spend.
fig, ax1 = plt.subplots(figsize = (9,6),sharex=True)
BundleFc_Outcome['Spend'].plot(kind = 'bar',color = 'blue',width = 0.4, ax = ax1,position = 1)
#
# Make the y-axis label, ticks and tick labels match the line color.
ax1.set_ylabel('SPEND', color='b', size = 18)
ax1.set_xlabel('Region',color='w',size = 18)
ax2 = ax1.twinx()
ax2.set_ylabel('ROAS', color='r',size = 18)
ax1.tick_params(axis='x', colors='w',size = 20)
ax2.tick_params(axis = 'y', colors='w',size = 20)
ax1.tick_params(axis = 'y', colors='w',size = 20)
#ax1.text()
#
BundleFc_Outcome['ROAS'].plot(kind = 'bar',color = 'red',width = 0.4, ax = ax2,position = 0.25)
plt.grid()
#ax2.set_ylim(0, 4000)
ax2.set_ylim(0,300)
plt.title('ROAS & SPEND By Region',color = 'w',size= 20)
fig.legend([ax2,ax1],labels = ['SPEND','ROAS'],loc = 0)
plt.hlines([100,20],xmin = 0,xmax = 8,color= ['r','b'])
I don't recommend using the builtin functions of pandas to do more complex plotting. Also when asking a question it is common courtesy to provide a minimal and verifiable example (see here). I took the liberty to simulate your problem.
Due to the change in axes, we need to generate our own legend. First the results:
Which can be achieved with:
import matplotlib.pyplot as plt, pandas as pd, numpy as np
# generate dummy data.
X = np.random.rand(10, 2)
X[:,1] *= 1000
x = np.arange(X.shape[0]) * 2 # xticks
df = pd.DataFrame(X, columns = 'Spend Roast'.split())
# end dummy data
fig, ax1 = plt.subplots(figsize = (9,6),sharex=True)
ax2 = ax1.twinx()
# tmp axes
axes = [ax1, ax2] # setup axes
colors = plt.cm.tab20(x)
width = .5 # bar width
# generate dummy legend
elements = []
# plot data
for idx, col in enumerate(df.columns):
tax = axes[idx]
tax.bar(x + idx * width, df[col], label = col, width = width, color = colors[idx])
element = tax.Line2D([0], [0], color = colors[idx], label = col) # setup dummy label
elements.append(element)
# desired hline
tax.axhline(200, color = 'red')
tax.set(xlabel = 'Bundle FC', ylabel = 'ROAST')
axes[0].set_ylabel('SPEND')
tax.legend(handles = elements)