Custom labels in Chaco Legend - python

I'd like to change the line labels on a chaco Legend because my labels need to be ascending floats:
1,2,3,4
But it is string sorting, so I'm getting:
1, 10, 11, 2, 21 etc...
I noticed the documentation seems unfinished in regard to this:
http://chaco.readthedocs.org/en/latest/user_manual/basic_elements/overlays.html#legend
I've tried setting the legends labels manually:
self.plot.legend.labels = list([i for i in self.mylist])
I'm using a colormap, so this is very noticeable as the legend shows blue lines and red lines mixed seemingly randomly due to the string sorting.
Below is a minimal working example
This example does not use the same colormap I'm using, but shows how the line ordering in the legend is not sorted. It's not important which colormap is used, what's important is the string sorting in the legend gives unwanted aesthetics.
from traits.api import *
from chaco.api import *
from traitsui.api import *
from chaco.example_support import COLOR_PALETTE
from enable.api import ComponentEditor
import numpy as np
class TestPlot(HasTraits):
plot = Instance(Plot)
traits_view = View( Item('plot', editor=ComponentEditor(), show_label=False) )
def _plot_default(self):
data = ArrayPlotData()
plot = Plot(data)
x = np.linspace(0,10,100)
data.set_data('x', x)
for i, freq in enumerate(range(1,20,3)):
y = 'line_%s' % freq
color = tuple(COLOR_PALETTE[i])
data.set_data(y, i*x)
plot.plot(('x', y), name=y, color=color)
plot.legend.visible = True
return plot
if __name__ == '__main__':
TestPlot().configure_traits()
See screenshot:

To sort your labels properly you need just to apply natural sorting. Install "natsort" library and insert two lines in your code:
from natsort import natsorted
...
plot.legend.labels = natsorted(plot.plots.keys())
This will do the trick.

You can add leading zeros for one digit numbers by changing the line
y = 'line_%s' % freq
to
y = 'line_%02d' % freq
I assume you have no more than 99 graphs otherwise you need to change the 02 to 03. Then your legend should be correctly sorted.
See https://docs.python.org/3.4/library/string.html#format-specification-mini-language for more information on the string format specifiers.
The format 0# where # is a number, means that in the string the number uses # positions and if the number is smaller than the given width it is filled with trailing zeros. If you want floating point numbers with one digit as fractional part use %04.1f

Related

Matplotlib - Several lines on the same plot

I am converting some old Python 2.7 code to 3.6.
My routine plots the first line OK but subsequent lines seem to start where the previous line left off. (Running on-line at www.pythonanywhere.com)
My code:
import matplotlib
from matplotlib import pyplot;
k = 0
while k < len(Stations):
# Draw the graph
fig.patch.set_facecolor('black') # Outside border
pyplot.rcParams['axes.facecolor'] = 'black' # Graph background
pyplot.rcParams['axes.edgecolor'] = 'red'
pyplot.tick_params(axis='x', colors='yellow')
pyplot.tick_params(axis='y', colors='yellow')
pyplot.ylim(float(BtmLimit),float(TopLimit))
pyplot.ylabel("Percent of normal range.", size=10, color = "yellow")
pyplot.xticks([]) # Hide X axis
pyplot.title("Plotted at %sGMT, %s %s %s" % (thour, tday, tdate, tmonth), color = "yellow")
if Error == 'False': pyplot.plot(Epoch, Scaled, color = (Color), linewidth=1.9)
pyplot.plot(Epoch, Top, color = [0,0.5,0]) # Green lines
pyplot.plot(Epoch, Btm, color = [0,0.5,0])
k = k + 1
pyplot.savefig(SD+'RiverLevels.png', facecolor='black', bbox_inches='tight')
pyplot.show()
pyplot.close()
The data looks like this:
Epoch
['1638046800', '1638047700', '1638048600', '1638049500', '1638050400', '1638051300', '1638052200', '1638053100', '1638054000', '1638054900', '1638
055800', '1638056700', '1638057600', '1638058500', '1638059400', '1638060300', '1638061200', '1638062100', '1638063000', '1638063900', '1638064800
', '1638065700', '1638066600', '1638067500', '1638068400', '1638069300', '1638070200', '1638071100', '1638072000', '1638072900', '1638073800', '16
38074700', '1638075600', '1638076500', '1638077400', '1638078300', '1638079200', '1638080100', '1638081000', '1638081900', '1638082800', '16380837
00', '1638084600', '1638085500', '1638086400', '1638087300', '1638088200', '1638089100', '1638090000', '1638090900', '1638091800', '1638092700', '
1638093600', '1638094500', '1638095400']
Scaled
['32.475247524752476', '33.069306930693074', '33.76237623762376', '33.56435643564357', '33.56435643564357', '33.86138613861387', '34.1584158415841
6', '34.35643564356436', '34.554455445544555', '34.554455445544555', '34.75247524752476', '34.95049504950495', '35.049504950495056', '35.148514851
48515', '35.049504950495056', '35.14851485148515', '35.44554455445545', '35.54455445544555', '35.54455445544555', '35.34653465346535', '35.5445544
5544555', '35.64356435643565', '35.84158415841585', '35.742574257425744', '35.54455445544555', '35.44554455445545', '35.44554455445545', '35.34653
465346535', '35.24752475247525', '35.049504950495056', '34.95049504950495', '34.95049504950495', '34.851485148514854', '34.65346534653466', '34.35
643564356436', '34.15841584158416', '34.35643564356436', '34.35643564356436', '34.25742574257426', '34.05940594059406', '33.86138613861387', '33.6
63366336633665', '33.86138613861387', '33.663366336633665', '33.663366336633665', '33.46534653465347', '33.366336633663366', '33.56435643564357',
'33.663366336633665', '33.663366336633665', '33.663366336633665', '33.663366336633665', '33.960396039603964', '34.05940594059406', '34.05940594059
406']
Output image
I guess this may be due to using strings instead of numbers. When you use strings, the x values are taken as categories and not ordered numerically but in the order they appear in the list (unless a category is exactly repeated). I understand that the snippet is not complete, but the values of Epoch and Scaled actually change on each iteration.
After plotting the first set of data, any values not present in the first set will be positioned "afterwards" those of the first set (ie: to the right of first set's last point in x, and higher than the last point in y). When the second set of data is plotted, the first x values have not appeared in the previous set, so they are plotted afterwards (beginning of light blue line in the plot), regardless of their numeric value. Then, the final values are the same of those that had appeared in the first set, so the line goes back to the left of the figure.
You can try using [float(x) for x in Epoch] and [float(y) for y in Scaled] in the plots. As I see that there are spaces in the strings representing the numbers, you could use a function like this:
def flist_from_slist(data):
return [float(x.replace(' ', '')) for x in data]
And replace the pyplot.plot call by:
pyplot.plot(flist_from_slist(Epoch), flist_from_slist(Scaled), linewidth=1.9)
Moreover, there is a lot of code inside the loop that could be moved outside (setting the ticks, labels, etc).

How to plot dotted lines from a shapefile in python?

I am not sure on how to plot a dotted line from a shapefile in Python. It appears that readshapefile() does not have any linestyle for me to set. Below I have a working code where I take a shapefile and plot it, but it only plots a solid line. Any ideas to set me in the right direction? Thanks!
The shapefile can be found here: http://www.natice.noaa.gov/products/daily_products.html, where the Start Date is Feb 15th, end date is Feb 17th, and the Date Types is Ice Edge. It should be the first link.
#!/awips2/python/bin/python
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
map = Basemap(llcrnrlon=-84.37,llcrnrlat=42.11,urcrnrlon=-20.93,urcrnrlat=66.48,
resolution='i', projection='tmerc', lat_0 = 55., lon_0 = -50.)
map.drawmapboundary(fill_color='aqua')
map.fillcontinents(color='#ddaa66',lake_color='aqua')
map.drawcoastlines(zorder = 3)
map.readshapefile('nic_autoc2018046n_pl_a', 'IceEdge', zorder = 2, color = 'blue')
plt.show()
From the Basemap documentation:
A tuple (num_shapes, type, min, max) containing shape file info is
returned. num_shapes is the number of shapes, type is the type code
(one of the SHPT* constants defined in the shapelib module, see
http://shapelib.maptools.org/shp_api.html) and min and max are
4-element lists with the minimum and maximum values of the vertices.
If drawbounds=True a matplotlib.patches.LineCollection object is
appended to the tuple.
drawbounds is True by default, so all you have to do is collect the return value of readshapefile and alter the linestyle of the returned LineCollection object, which can be done with LineCollection.set_linestyle(). So in principle you can change the linestyle of your plotted shape file with something like this:
result = m.readshapefile('shapefiles/nic_autoc2018046n_pl_a', 'IceEdge', zorder = 10, color = 'blue')#, drawbounds = False)
col = result[-1]
col.set_linestyle('dotted')
plt.show()
However, your shapefile contains 5429 separate line segments of different length and somehow matplotlib does not seem to be able to deal with this large amount of non-continuous lines. At least on my machine the plotting did not finish within one hour, so I interrupted the process. I played a bit with your file and it seems like many of the lines are broken into segments unnecessarily (I'm guessing this is because the ice sheet outlines are somehow determined on tiles and then pieced together afterwards, but only the providers will really know). Maybe it would help to piece together adjacent pieces, but I'm not sure.
I was also wondering whether the result would even look that great with a dotted line, because there are so many sharp bends. Below I show a picture where I only plot the 100 longest line segments (leaving out drawcoastlines and with thicker lines) using this code:
import numpy as np
result = m.readshapefile('shapefiles/nic_autoc2018046n_pl_a', 'IceEdge', zorder = 10, color = 'blue')#, drawbounds = False)
col = result[-1]
segments = col.get_segments()
seglens = [len(seg) for seg in col.get_segments()]
segments = np.array(segments)
seglens = np.array(seglens)
idx = np.argsort(seglens)
seglens = seglens[idx]
segments = segments[idx]
col.remove()
new_col = LineCollection(segments[-100:],linewidths = 2, linestyles='dotted', colors='b')
ax.add_collection(new_col)
plt.show()
And the result looks like this:

How to avoid keys with zero percentage in pie plot matplotlib

I have to plot pie chart with %age values, I am facing a problem that some value are very small and their %age is about zero, when I plot using matplotlib in python, therir labels overlab and they are not readable. I think its one solution is to avoid values with zero %age and second is to seprate labels to overlap (with some arrow etc.) Here is my simple code
def show_pi_chart(plot_title,keys,values,save_file):
size = len(keys)
#Get Colors list
color_list = make_color_list(size)
pyplot.axis("equal")
pyplot.pie(values,
labels=keys,
colors=color_list,
autopct="%1.1f%%"
)
pyplot.title(plot_title)
pyplot.show()
And my chart is
What is the solution to make labels dictant or remove small %age keys
The following code should work as intended:
from matplotlib import pyplot
from collections import Counter
import numpy as np
def fixOverLappingText(text):
# if undetected overlaps reduce sigFigures to 1
sigFigures = 2
positions = [(round(item.get_position()[1],sigFigures), item) for item in text]
overLapping = Counter((item[0] for item in positions))
overLapping = [key for key, value in overLapping.items() if value >= 2]
for key in overLapping:
textObjects = [text for position, text in positions if position == key]
if textObjects:
# If bigger font size scale will need increasing
scale = 0.05
spacings = np.linspace(0,scale*len(textObjects),len(textObjects))
for shift, textObject in zip(spacings,textObjects):
textObject.set_y(key + shift)
def show_pi_chart(plot_title,keys,values):
pyplot.axis("equal")
# make sure to assign text variable to index [1] of return values
text = pyplot.pie(values, labels=keys, autopct="%1.1f%%")[1]
fixOverLappingText(text)
pyplot.title(plot_title)
pyplot.show()
show_pi_chart("TITLE",("One","Two","Three","Four","Five","Six","Seven", "Eight"),(20,0,0,10,44,0,0,44))

Manually setting xticks with xaxis_date() in Python/matplotlib

I've been looking into how to make plots against time on the x axis and have it pretty much sorted, with one strange quirk that makes me wonder whether I've run into a bug or (admittedly much more likely) am doing something I don't really understand.
Simply put, below is a simplified version of my program. If I put this in a .py file and execute it from an interpreter (ipython) I get a figure with an x axis with the year only, "2012", repeated a number of times, like this.
However, if I comment out the line (40) that sets the xticks manually, namely 'plt.xticks(tk)' and then run that exact command in the interpreter immediately after executing the script, it works great and my figure looks like this.
Similarly it also works if I just move that line to be after the savefig command in the script, that's to say to put it at the very end of the file. Of course in both cases only the figure drawn on screen will have the desired axis, and not the saved file. Why can't I set my x axis earlier?
Grateful for any insights, thanks in advance!
import matplotlib.pyplot as plt
import datetime
# define arrays for x, y and errors
x=[16.7,16.8,17.1,17.4]
y=[15,17,14,16]
e=[0.8,1.2,1.1,0.9]
xtn=[]
# convert x to datetime format
for t in x:
hours=int(t)
mins=int((t-int(t))*60)
secs=int(((t-hours)*60-mins)*60)
dt=datetime.datetime(2012,01,01,hours,mins,secs)
xtn.append(date2num(dt))
# set up plot
fig=plt.figure()
ax=fig.add_subplot(1,1,1)
# plot
ax.errorbar(xtn,y,yerr=e,fmt='+',elinewidth=2,capsize=0,color='k',ecolor='k')
# set x axis range
ax.xaxis_date()
t0=date2num(datetime.datetime(2012,01,01,16,35)) # x axis startpoint
t1=date2num(datetime.datetime(2012,01,01,17,35)) # x axis endpoint
plt.xlim(t0,t1)
# manually set xtick values
tk=[]
tk.append(date2num(datetime.datetime(2012,01,01,16,40)))
tk.append(date2num(datetime.datetime(2012,01,01,16,50)))
tk.append(date2num(datetime.datetime(2012,01,01,17,00)))
tk.append(date2num(datetime.datetime(2012,01,01,17,10)))
tk.append(date2num(datetime.datetime(2012,01,01,17,20)))
tk.append(date2num(datetime.datetime(2012,01,01,17,30)))
plt.xticks(tk)
plt.show()
# save to file
plt.savefig('savefile.png')
I don't think you need that call to xaxis_date(); since you are already providing the x-axis data in a format that matplotlib knows how to deal with. I also think there's something slightly wrong with your secs formula.
We can make use of matplotlib's built-in formatters and locators to:
set the major xticks to a regular interval (minutes, hours, days, etc.)
customize the display using a strftime formatting string
It appears that if a formatter is not specified, the default is to display the year; which is what you were seeing.
Try this out:
import datetime as dt
import matplotlib.pyplot as plt
from matplotlib.dates import DateFormatter, MinuteLocator
x = [16.7,16.8,17.1,17.4]
y = [15,17,14,16]
e = [0.8,1.2,1.1,0.9]
xtn = []
for t in x:
h = int(t)
m = int((t-int(t))*60)
xtn.append(dt.datetime.combine(dt.date(2012,1,1), dt.time(h,m)))
def larger_alim( alim ):
''' simple utility function to expand axis limits a bit '''
amin,amax = alim
arng = amax-amin
nmin = amin - 0.1 * arng
nmax = amax + 0.1 * arng
return nmin,nmax
plt.errorbar(xtn,y,yerr=e,fmt='+',elinewidth=2,capsize=0,color='k',ecolor='k')
plt.gca().xaxis.set_major_locator( MinuteLocator(byminute=range(0,60,10)) )
plt.gca().xaxis.set_major_formatter( DateFormatter('%H:%M:%S') )
plt.gca().set_xlim( larger_alim( plt.gca().get_xlim() ) )
plt.show()
Result:
FWIW the utility function larger_alim was originally written for this other question: Is there a way to tell matplotlib to loosen the zoom on the plotted data?

Create a color generator from given colormap in matplotlib

I have a series of lines that each need to be plotted with a separate colour. Each line is actually made up of several data sets (positive, negative regions etc.) and so I'd like to be able to create a generator that will feed one colour at a time across a spectrum, for example the gist_rainbow map shown here.
I have found the following works but it seems very complicated and more importantly difficult to remember,
from pylab import *
NUM_COLORS = 22
mp = cm.datad['gist_rainbow']
get_color = matplotlib.colors.LinearSegmentedColormap.from_list(mp, colors=['r', 'b'], N=NUM_COLORS)
...
# Then in a for loop
this_color = get_color(float(i)/NUM_COLORS)
Moreover, it does not cover the range of colours in the gist_rainbow map, I have to redefine a map.
Maybe a generator is not the best way to do this, if so what is the accepted way?
To index colors from a specific colormap you can use:
import pylab
NUM_COLORS = 22
cm = pylab.get_cmap('gist_rainbow')
for i in range(NUM_COLORS):
color = cm(1.*i/NUM_COLORS) # color will now be an RGBA tuple
# or if you really want a generator:
cgen = (cm(1.*i/NUM_COLORS) for i in range(NUM_COLORS))

Categories

Resources