I am trying to draw wind roses with the following code. It used to work a few months ago.
import sys
from windrose import WindroseAxes
from matplotlib import pyplot as plt
import matplotlib.cm as cm
from numpy.random import random
from numpy import arange
import os
import numpy as np
def plot(prefix, spds, dirs):
ws = np.array(spds)
wd = np.array(dirs)
def new_axes():
fig = plt.figure(figsize=(8, 8), dpi=80, facecolor='w', edgecolor='w')
rect = [0.1, 0.1, 0.8, 0.8]
ax = WindroseAxes(fig, rect, axisbg='w')
fig.add_axes(ax)
return ax, fig
def set_legend(ax):
l = ax.legend(axespad=-0.10, title="m/s", loc=0)
plt.setp(l.get_texts(), fontsize=8)
# windrose like a stacked histogram with normed (displayed in percent) results
ax, fig = new_axes()
ax.bar(wd, ws, normed=True, opening=0.8, edgecolor='white', bins=arange(0,max(ws),5))
set_legend(ax)
tokens = prefix.split("/")[-1].split("_")
if tokens[0] == "Dust":
title = "%s Dust" % tokens[1]
else:
title = tokens[0]
plt.title(title, y=1.08)
fig.savefig("%s-fig1.png" % prefix)
def main(folder="data"):
for filename in filter(lambda x:x.endswith(".csv"), os.listdir(folder)):
path = "%s/%s" % (folder, filename)
plot_path = "%s/plots" % folder
if not os.path.exists(plot_path):
os.mkdir(plot_path)
print path
f = open(path)
f.readline()
directions = []
speeds = []
for line in f:
cols = line.split(",")
direction = cols[5]
speed = cols[6]
try:
direction = int(direction)
speed = int(speed) * 0.44704
except:
continue
directions.append(direction)
speeds.append(speed)
plot("%s/plots/%s" % (folder, filename.split(".")[0]), speeds, directions)
if __name__ == "__main__":
main(sys.argv[1])
But when I run it I get this error:
IndexError Traceback (most recent call last)
/Users/Abdulhaleem-Labban/Dropbox/windrose/process.py in <module>()
112
113 if __name__ == "__main__":
--> 114 main(sys.argv[1])
115
IndexError: list index out of range
The file that contains the data needed to draw the wind roses is called Dust_TabukWI_Wind.csv. Here's a sample:
Tabuk YR-- MO DA HRMN DIR SPD VSB MW
403750 1985 1 1 1125 240 28 0.4 32
403750 1985 1 18 1200 230 34 0.1 33
403750 1985 12 18 600 120 14 6.2 30
403750 1988 11 30 1300 340 34 0.3 32
403750 1992 12 15 900 240 31 0.3 33
403750 1992 12 15 1000 240 29 0.3 33
403750 1992 12 15 1100 240 29 0.6 33
403750 2008 1 29 1100 220 29 0.6 31
403750 2008 1 29 1200 210 34 3.1 30
403750 2008 1 29 1300 210 34 1.9 31
I think this is the script that has the "argv" but how can i run it ?
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__version__ = '1.4'
__author__ = 'Lionel Roubeyrie'
__mail__ = 'lionel.roubeyrie#gmail.com'
__license__ = 'CeCILL-B'
import matplotlib
import matplotlib.cm as cm
import numpy as np
from matplotlib.patches import Rectangle, Polygon
from matplotlib.ticker import ScalarFormatter, AutoLocator
from matplotlib.text import Text, FontProperties
from matplotlib.projections.polar import PolarAxes
from numpy.lib.twodim_base import histogram2d
import matplotlib.pyplot as plt
from pylab import poly_between
RESOLUTION = 100
ZBASE = -1000 #The starting zorder for all drawing, negative to have the grid on
class WindroseAxes(PolarAxes):
"""
Create a windrose axes
"""
def __init__(self, *args, **kwargs):
"""
See Axes base class for args and kwargs documentation
"""
#Uncomment to have the possibility to change the resolution directly
#when the instance is created
#self.RESOLUTION = kwargs.pop('resolution', 100)
PolarAxes.__init__(self, *args, **kwargs)
self.set_aspect('equal', adjustable='box', anchor='C')
self.radii_angle = 67.5
self.cla()
def cla(self):
"""
Clear the current axes
"""
PolarAxes.cla(self)
self.theta_angles = np.arange(0, 360, 45)
self.theta_labels = ['E', 'N-E', 'N', 'N-W', 'W', 'S-W', 'S', 'S-E']
self.set_thetagrids(angles=self.theta_angles, labels=self.theta_labels)
self._info = {'dir' : list(),
'bins' : list(),
'table' : list()}
self.patches_list = list()
def _colors(self, cmap, n):
'''
Returns a list of n colors based on the colormap cmap
'''
return [cmap(i) for i in np.linspace(0.0, 1.0, n)]
def set_radii_angle(self, **kwargs):
"""
Set the radii labels angle
"""
null = kwargs.pop('labels', None)
angle = kwargs.pop('angle', None)
if angle is None:
angle = self.radii_angle
self.radii_angle = angle
print self.get_rmax()
radii = np.linspace(0.1, self.get_rmax(), 6)
radii_labels = [ "%.1f%%" %r for r in radii ]
radii_labels[0] = "" #Removing label 0
# radii_labels = ["" for r in radii]
null = self.set_rgrids(radii=radii, labels=radii_labels,
angle=self.radii_angle, **kwargs)
def _update(self):
self.set_rmax(rmax=np.max(np.sum(self._info['table'], axis=0)))
self.set_radii_angle(angle=self.radii_angle)
def legend(self, loc='lower left', **kwargs):
"""
Sets the legend location and her properties.
The location codes are
'best' : 0,
'upper right' : 1,
'upper left' : 2,
'lower left' : 3,
'lower right' : 4,
'right' : 5,
'center left' : 6,
'center right' : 7,
'lower center' : 8,
'upper center' : 9,
'center' : 10,
If none of these are suitable, loc can be a 2-tuple giving x,y
in axes coords, ie,
loc = (0, 1) is left top
loc = (0.5, 0.5) is center, center
and so on. The following kwargs are supported:
isaxes=True # whether this is an axes legend
prop = FontProperties(size='smaller') # the font property
pad = 0.2 # the fractional whitespace inside the legend border
shadow # if True, draw a shadow behind legend
labelsep = 0.005 # the vertical space between the legend entries
handlelen = 0.05 # the length of the legend lines
handletextsep = 0.02 # the space between the legend line and legend text
axespad = 0.02 # the border between the axes and legend edge
"""
def get_handles():
handles = list()
for p in self.patches_list:
if isinstance(p, matplotlib.patches.Polygon) or \
isinstance(p, matplotlib.patches.Rectangle):
color = p.get_facecolor()
elif isinstance(p, matplotlib.lines.Line2D):
color = p.get_color()
else:
raise AttributeError("Can't handle patches")
handles.append(Rectangle((0, 0), 0.2, 0.2,
facecolor=color, edgecolor='black'))
return handles
def get_labels():
labels = np.copy(self._info['bins'])
labels = ["[%.1f : %0.1f[" %(labels[i], labels[i+1]) \
for i in range(len(labels)-1)]
return labels
null = kwargs.pop('labels', None)
null = kwargs.pop('handles', None)
handles = get_handles()
labels = get_labels()
self.legend_ = matplotlib.legend.Legend(self, handles, labels,
loc, **kwargs)
return self.legend_
def _init_plot(self, dir, var, **kwargs):
"""
Internal method used by all plotting commands
"""
#self.cla()
null = kwargs.pop('zorder', None)
#Init of the bins array if not set
bins = kwargs.pop('bins', None)
if bins is None:
bins = np.linspace(np.min(var), np.max(var), 6)
if isinstance(bins, int):
#bins = np.linspace(np.min(var), np.max(var), bins)
bins = [0.0,5.0,10.0,15.0,20.0,25.0,30.0,35.0,40.0]
bins = np.asarray(bins)
nbins = len(bins)
#Number of sectors
nsector = kwargs.pop('nsector', None)
if nsector is None:
nsector = 16
#Sets the colors table based on the colormap or the "colors" argument
colors = kwargs.pop('colors', None)
cmap = kwargs.pop('cmap', None)
if colors is not None:
if isinstance(colors, str):
colors = [colors]*nbins
if isinstance(colors, (tuple, list)):
if len(colors) != nbins:
raise ValueError("colors and bins must have same length")
else:
if cmap is None:
cmap = cm.jet
colors = self._colors(cmap, nbins)
#Building the angles list
angles = np.arange(0, -2*np.pi, -2*np.pi/nsector) + np.pi/2
normed = kwargs.pop('normed', False)
blowto = kwargs.pop('blowto', False)
#Set the global information dictionnary
self._info['dir'], self._info['bins'], self._info['table'] = histogram(dir, var, bins, nsector, normed, blowto)
return bins, nbins, nsector, colors, angles, kwargs
def contour(self, dir, var, **kwargs):
"""
Plot a windrose in linear mode. For each var bins, a line will be
draw on the axes, a segment between each sector (center to center).
Each line can be formated (color, width, ...) like with standard plot
pylab command.
Mandatory:
* dir : 1D array - directions the wind blows from, North centred
* var : 1D array - values of the variable to compute. Typically the wind
speeds
Optional:
* nsector: integer - number of sectors used to compute the windrose
table. If not set, nsectors=16, then each sector will be 360/16=22.5°,
and the resulting computed table will be aligned with the cardinals
points.
* bins : 1D array or integer- number of bins, or a sequence of
bins variable. If not set, bins=6, then
bins=linspace(min(var), max(var), 6)
* blowto : bool. If True, the windrose will be pi rotated,
to show where the wind blow to (usefull for pollutant rose).
* colors : string or tuple - one string color ('k' or 'black'), in this
case all bins will be plotted in this color; a tuple of matplotlib
color args (string, float, rgb, etc), different levels will be plotted
in different colors in the order specified.
* cmap : a cm Colormap instance from matplotlib.cm.
- if cmap == None and colors == None, a default Colormap is used.
others kwargs : see help(pylab.plot)
"""
bins, nbins, nsector, colors, angles, kwargs = self._init_plot(dir, var,
**kwargs)
#closing lines
angles = np.hstack((angles, angles[-1]-2*np.pi/nsector))
vals = np.hstack((self._info['table'],
np.reshape(self._info['table'][:,0],
(self._info['table'].shape[0], 1))))
offset = 0
for i in range(nbins):
val = vals[i,:] + offset
offset += vals[i, :]
zorder = ZBASE + nbins - i
patch = self.plot(angles, val, color=colors[i], zorder=zorder,
**kwargs)
self.patches_list.extend(patch)
self._update()
def contourf(self, dir, var, **kwargs):
"""
Plot a windrose in filled mode. For each var bins, a line will be
draw on the axes, a segment between each sector (center to center).
Each line can be formated (color, width, ...) like with standard plot
pylab command.
Mandatory:
* dir : 1D array - directions the wind blows from, North centred
* var : 1D array - values of the variable to compute. Typically the wind
speeds
Optional:
* nsector: integer - number of sectors used to compute the windrose
table. If not set, nsectors=16, then each sector will be 360/16=22.5°,
and the resulting computed table will be aligned with the cardinals
points.
* bins : 1D array or integer- number of bins, or a sequence of
bins variable. If not set, bins=6, then
bins=linspace(min(var), max(var), 6)
* blowto : bool. If True, the windrose will be pi rotated,
to show where the wind blow to (usefull for pollutant rose).
* colors : string or tuple - one string color ('k' or 'black'), in this
case all bins will be plotted in this color; a tuple of matplotlib
color args (string, float, rgb, etc), different levels will be plotted
in different colors in the order specified.
* cmap : a cm Colormap instance from matplotlib.cm.
- if cmap == None and colors == None, a default Colormap is used.
others kwargs : see help(pylab.plot)
"""
bins, nbins, nsector, colors, angles, kwargs = self._init_plot(dir, var,
**kwargs)
null = kwargs.pop('facecolor', None)
null = kwargs.pop('edgecolor', None)
#closing lines
angles = np.hstack((angles, angles[-1]-2*np.pi/nsector))
vals = np.hstack((self._info['table'],
np.reshape(self._info['table'][:,0],
(self._info['table'].shape[0], 1))))
offset = 0
for i in range(nbins):
val = vals[i,:] + offset
offset += vals[i, :]
zorder = ZBASE + nbins - i
xs, ys = poly_between(angles, 0, val)
patch = self.fill(xs, ys, facecolor=colors[i],
edgecolor=colors[i], zorder=zorder, **kwargs)
self.patches_list.extend(patch)
def bar(self, dir, var, **kwargs):
"""
Plot a windrose in bar mode. For each var bins and for each sector,
a colored bar will be draw on the axes.
Mandatory:
* dir : 1D array - directions the wind blows from, North centred
* var : 1D array - values of the variable to compute. Typically the wind
speeds
Optional:
* nsector: integer - number of sectors used to compute the windrose
table. If not set, nsectors=16, then each sector will be 360/16=22.5°,
and the resulting computed table will be aligned with the cardinals
points.
* bins : 1D array or integer- number of bins, or a sequence of
bins variable. If not set, bins=6 between min(var) and max(var).
* blowto : bool. If True, the windrose will be pi rotated,
to show where the wind blow to (usefull for pollutant rose).
* colors : string or tuple - one string color ('k' or 'black'), in this
case all bins will be plotted in this color; a tuple of matplotlib
color args (string, float, rgb, etc), different levels will be plotted
in different colors in the order specified.
* cmap : a cm Colormap instance from matplotlib.cm.
- if cmap == None and colors == None, a default Colormap is used.
edgecolor : string - The string color each edge bar will be plotted.
Default : no edgecolor
* opening : float - between 0.0 and 1.0, to control the space between
each sector (1.0 for no space)
"""
bins, nbins, nsector, colors, angles, kwargs = self._init_plot(dir, var,
**kwargs)
null = kwargs.pop('facecolor', None)
edgecolor = kwargs.pop('edgecolor', None)
if edgecolor is not None:
if not isinstance(edgecolor, str):
raise ValueError('edgecolor must be a string color')
opening = kwargs.pop('opening', None)
if opening is None:
opening = 0.8
dtheta = 2*np.pi/nsector
opening = dtheta*opening
for j in range(nsector):
offset = 0
for i in range(nbins):
if i > 0:
offset += self._info['table'][i-1, j]
val = self._info['table'][i, j]
zorder = ZBASE + nbins - i
patch = Rectangle((angles[j]-opening/2, offset), opening, val,
facecolor=colors[i], edgecolor=edgecolor, zorder=zorder,
**kwargs)
self.add_patch(patch)
if j == 0:
self.patches_list.append(patch)
self._update()
def box(self, dir, var, **kwargs):
"""
Plot a windrose in proportional bar mode. For each var bins and for each
sector, a colored bar will be draw on the axes.
Mandatory:
* dir : 1D array - directions the wind blows from, North centred
* var : 1D array - values of the variable to compute. Typically the wind
speeds
Optional:
* nsector: integer - number of sectors used to compute the windrose
table. If not set, nsectors=16, then each sector will be 360/16=22.5°,
and the resulting computed table will be aligned with the cardinals
points.
* bins : 1D array or integer- number of bins, or a sequence of
bins variable. If not set, bins=6 between min(var) and max(var).
* blowto : bool. If True, the windrose will be pi rotated,
to show where the wind blow to (usefull for pollutant rose).
* colors : string or tuple - one string color ('k' or 'black'), in this
case all bins will be plotted in this color; a tuple of matplotlib
color args (string, float, rgb, etc), different levels will be plotted
in different colors in the order specified.
* cmap : a cm Colormap instance from matplotlib.cm.
- if cmap == None and colors == None, a default Colormap is used.
edgecolor : string - The string color each edge bar will be plotted.
Default : no edgecolor
"""
bins, nbins, nsector, colors, angles, kwargs = self._init_plot(dir, var,
**kwargs)
null = kwargs.pop('facecolor', None)
edgecolor = kwargs.pop('edgecolor', None)
if edgecolor is not None:
if not isinstance(edgecolor, str):
raise ValueError('edgecolor must be a string color')
opening = np.linspace(0.0, np.pi/16, nbins)
for j in range(nsector):
offset = 0
for i in range(nbins):
if i > 0:
offset += self._info['table'][i-1, j]
val = self._info['table'][i, j]
zorder = ZBASE + nbins - i
patch = Rectangle((angles[j]-opening[i]/2, offset), opening[i],
val, facecolor=colors[i], edgecolor=edgecolor,
zorder=zorder, **kwargs)
self.add_patch(patch)
if j == 0:
self.patches_list.append(patch)
self._update()
def histogram(dir, var, bins, nsector, normed=False, blowto=False):
"""
Returns an array where, for each sector of wind
(centred on the north), we have the number of time the wind comes with a
particular var (speed, polluant concentration, ...).
* dir : 1D array - directions the wind blows from, North centred
* var : 1D array - values of the variable to compute. Typically the wind
speeds
* bins : list - list of var category against we're going to compute the table
* nsector : integer - number of sectors
* normed : boolean - The resulting table is normed in percent or not.
* blowto : boolean - Normaly a windrose is computed with directions
as wind blows from. If true, the table will be reversed (usefull for
pollutantrose)
"""
if len(var) != len(dir):
raise ValueError, "var and dir must have same length"
angle = 360./nsector
dir_bins = np.arange(-angle/2 ,360.+angle, angle, dtype=np.float)
dir_edges = dir_bins.tolist()
dir_edges.pop(-1)
dir_edges[0] = dir_edges.pop(-1)
dir_bins[0] = 0.
var_bins = bins.tolist()
var_bins.append(np.inf)
if blowto:
dir = dir + 180.
dir[dir>=360.] = dir[dir>=360.] - 360
table = histogram2d(x=var, y=dir, bins=[var_bins, dir_bins],
normed=False)[0]
# add the last value to the first to have the table of North winds
table[:,0] = table[:,0] + table[:,-1]
# and remove the last col
table = table[:, :-1]
if normed:
table = table*100/table.sum()
return dir_edges, var_bins, table
def wrcontour(dir, var, **kwargs):
fig = plt.figure()
rect = [0.1, 0.1, 0.8, 0.8]
ax = WindroseAxes(fig, rect)
fig.add_axes(ax)
ax.contour(dir, var, **kwargs)
l = ax.legend(axespad=-0.10)
plt.setp(l.get_texts(), fontsize=8)
plt.draw()
plt.show()
return ax
def wrcontourf(dir, var, **kwargs):
fig = plt.figure()
rect = [0.1, 0.1, 0.8, 0.8]
ax = WindroseAxes(fig, rect)
fig.add_axes(ax)
ax.contourf(dir, var, **kwargs)
l = ax.legend(axespad=-0.10)
plt.setp(l.get_texts(), fontsize=8)
plt.draw()
plt.show()
return ax
def wrbox(dir, var, **kwargs):
fig = plt.figure()
rect = [0.1, 0.1, 0.8, 0.8]
ax = WindroseAxes(fig, rect)
fig.add_axes(ax)
ax.box(dir, var, **kwargs)
l = ax.legend(axespad=-0.10)
plt.setp(l.get_texts(), fontsize=8)
plt.draw()
plt.show()
return ax
def wrbar(dir, var, **kwargs):
fig = plt.figure()
rect = [0.1, 0.1, 0.8, 0.8]
ax = WindroseAxes(fig, rect)
fig.add_axes(ax)
ax.bar(dir, var, **kwargs)
l = ax.legend(axespad=-0.10)
plt.setp(l.get_texts(), fontsize=8)
plt.draw()
plt.show()
return ax
def clean(dir, var):
'''
Remove masked values in the two arrays, where if a direction data is masked,
the var data will also be removed in the cleaning process (and vice-versa)
'''
dirmask = dir.mask==False
varmask = var.mask==False
ind = dirmask*varmask
return dir[ind], var[ind]
if __name__=='__main__':
from pylab import figure, show, setp, random, grid, draw
vv=random(500)*6
dv=random(500)*360
fig = figure(figsize=(8, 8), dpi=80, facecolor='w', edgecolor='w')
rect = [0.1, 0.1, 0.8, 0.8]
ax = WindroseAxes(fig, rect, axisbg='w')
fig.add_axes(ax)
# ax.contourf(dv, vv, bins=np.arange(0,8,1), cmap=cm.hot)
# ax.contour(dv, vv, bins=np.arange(0,8,1), colors='k')
# ax.bar(dv, vv, normed=True, opening=0.8, edgecolor='white')
ax.box(dv, vv, normed=True)
l = ax.legend(axespad=-0.10)
setp(l.get_texts(), fontsize=8)
draw()
#print ax._info
show()
sys.argv is the list of command-line arguments. sys.argv[0] is the command itself, sys.argv[1] is the first argument, etc. If there were no arguments, just the command, sys.argv will have only one element, sys.argv[0], and trying to read sys.argv[1] is out of range.
Normally, if your program requires an argument, you check for this and print a usage message:
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.stderr.write("Usage: {0} <data-path>\n".format(sys.argv[0]))
sys.exit(1)
main(sys.argv[1])
Related
I have some data where i have languages and relative unit size. I want to produce a bubble plot and then export it to PGF. I got most of my code from this answer Making a non-overlapping bubble chart in Matplotlib (circle packing) but I am having the problem that my text exits the circle boundary:
How can I either, increase the scale of everything (much easier I assume), or make sure that the bubble size is always greater than the text inside (and the bubbles are still proportional to each other according to the data series). I assume this is much more difficult to do but I don't really need that.
Relevant code:
#!/usr/bin/env python3
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
# create 10 circles with different radii
r = np.random.randint(5,15, size=10)
mapping = [("English", 25),
("French", 13),
("Spanish", 32),
("Thai", 10),
("Vientamese", 13),
("Chinese", 20),
("Jamaican", 8),
("Scottish", 3),
("Irish", 12),
("American", 5),
("Romanian", 3),
("Dutch", 2)]
class C():
def __init__(self,r):
self.colors = list(mcolors.XKCD_COLORS)
self.N = len(r)
self.labels = [item[0] for item in r]
self.x = np.ones((self.N,3))
self.x[:,2] = [item[1] for item in r]
maxstep = 2*self.x[:,2].max()
length = np.ceil(np.sqrt(self.N))
grid = np.arange(0,length*maxstep,maxstep)
gx,gy = np.meshgrid(grid,grid)
self.x[:,0] = gx.flatten()[:self.N]
self.x[:,1] = gy.flatten()[:self.N]
self.x[:,:2] = self.x[:,:2] - np.mean(self.x[:,:2], axis=0)
self.step = self.x[:,2].min()
self.p = lambda x,y: np.sum((x**2+y**2)**2)
self.E = self.energy()
self.iter = 1.
def minimize(self):
while self.iter < 1000*self.N:
for i in range(self.N):
rand = np.random.randn(2)*self.step/self.iter
self.x[i,:2] += rand
e = self.energy()
if (e < self.E and self.isvalid(i)):
self.E = e
self.iter = 1.
else:
self.x[i,:2] -= rand
self.iter += 1.
def energy(self):
return self.p(self.x[:,0], self.x[:,1])
def distance(self,x1,x2):
return np.sqrt((x1[0]-x2[0])**2+(x1[1]-x2[1])**2)-x1[2]-x2[2]
def isvalid(self, i):
for j in range(self.N):
if i!=j:
if self.distance(self.x[i,:], self.x[j,:]) < 0:
return False
return True
def scale(self, size):
"""Scales up the plot"""
self.x = self.x*size
def plot(self, ax):
for i in range(self.N):
circ = plt.Circle(self.x[i,:2],self.x[i,2], color=mcolors.XKCD_COLORS[self.colors[i]])
ax.add_patch(circ)
ax.text(self.x[i][0],self.x[i][1], self.labels[i], horizontalalignment='center', size='medium', color='black', weight='semibold')
c = C(mapping)
fig, ax = plt.subplots(subplot_kw=dict(aspect="equal"))
ax.axis("off")
c.minimize()
c.plot(ax)
ax.relim()
ax.autoscale_view()
plt.show()
I think both approaches that you outline are largely equivalent. In both cases, you have to determine the sizes of your text boxes in relation to the sizes of the circles. Getting precise bounding boxes for matplotlib text objects is tricky business, as rendering text objects is done by the backend, not matplotlib itself. So you have to render the text object, get its bounding box, compute the ratio between current and desired bounds, remove the text object, and finally re-render the text rescaled by the previously computed ratio. And since the bounding box computation and hence the rescaling is wildly inaccurate for very small and very large text objects, you actually have to repeat the process several times (below I am doing it twice, which is the minimum).
W.r.t. the placement of the circles, I have also taken the liberty of substituting your random walk in an energy landscape with a proper minimization. It's faster, and I think the results are much better.
#!/usr/bin/env python3
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from scipy.optimize import minimize, NonlinearConstraint
from scipy.spatial.distance import pdist, squareform
def _get_fontsize(size, label, ax, *args, **kwargs):
"""Given a circle, precompute the fontsize for a text object such that it fits the circle snuggly.
Parameters
----------
size : float
The radius of the circle.
label : str
The string.
ax : matplotlib.axis object
The matplotlib axis.
*args, **kwargs
Passed to ax.text().
Returns
-------
fontsize : float
The estimated fontsize.
"""
default_fontsize = kwargs.setdefault('size', plt.rcParams['font.size'])
width, height = _get_text_object_dimensions(ax, label, *args, **kwargs)
initial_estimate = size / (np.sqrt(width**2 + height**2) / 2) * default_fontsize
kwargs['size'] = initial_estimate
# Repeat process as bbox estimates are bad for very small and very large bboxes.
width, height = _get_text_object_dimensions(ax, label, *args, **kwargs)
return size / (np.sqrt(width**2 + height**2) / 2) * initial_estimate
def _get_text_object_dimensions(ax, string, *args, **kwargs):
"""Precompute the dimensions of a text object on a given axis in data coordinates.
Parameters
----------
ax : matplotlib.axis object
The matplotlib axis.
string : str
The string.
*args, **kwargs
Passed to ax.text().
Returns
-------
width, height : float
The dimensions of the text box in data units.
"""
text_object = ax.text(0., 0., string, *args, **kwargs)
renderer = _find_renderer(text_object.get_figure())
bbox_in_display_coordinates = text_object.get_window_extent(renderer)
bbox_in_data_coordinates = bbox_in_display_coordinates.transformed(ax.transData.inverted())
w, h = bbox_in_data_coordinates.width, bbox_in_data_coordinates.height
text_object.remove()
return w, h
def _find_renderer(fig):
"""
Return the renderer for a given matplotlib figure.
Notes
-----
Adapted from https://stackoverflow.com/a/22689498/2912349
"""
if hasattr(fig.canvas, "get_renderer"):
# Some backends, such as TkAgg, have the get_renderer method, which
# makes this easy.
renderer = fig.canvas.get_renderer()
else:
# Other backends do not have the get_renderer method, so we have a work
# around to find the renderer. Print the figure to a temporary file
# object, and then grab the renderer that was used.
# (I stole this trick from the matplotlib backend_bases.py
# print_figure() method.)
import io
fig.canvas.print_pdf(io.BytesIO())
renderer = fig._cachedRenderer
return(renderer)
class BubbleChart:
def __init__(self, sizes, colors, labels, ax=None, **font_kwargs):
# TODO: input sanitation
self.sizes = np.array(sizes)
self.labels = labels
self.colors = colors
self.ax = ax if ax else plt.gca()
self.positions = self._initialize_positions(self.sizes)
self.positions = self._optimize_positions(self.positions, self.sizes)
self._plot_bubbles(self.positions, self.sizes, self.colors, self.ax)
# NB: axis limits have to be finalized before computing fontsizes
self._rescale_axis(self.ax)
self._plot_labels(self.positions, self.sizes, self.labels, self.ax, **font_kwargs)
def _initialize_positions(self, sizes):
# TODO: try different strategies; set initial positions to lie
# - on a circle
# - on concentric shells, larger bubbles on the outside
return np.random.rand(len(sizes), 2) * np.min(sizes)
def _optimize_positions(self, positions, sizes):
# Adapted from: https://stackoverflow.com/a/73353731/2912349
def cost_function(new_positions, old_positions):
return np.sum((new_positions.reshape((-1, 2)) - old_positions)**2)
def constraint_function(x):
x = np.reshape(x, (-1, 2))
return pdist(x)
lower_bounds = sizes[np.newaxis, :] + sizes[:, np.newaxis]
lower_bounds -= np.diag(np.diag(lower_bounds)) # squareform requires zeros on diagonal
lower_bounds = squareform(lower_bounds)
nonlinear_constraint = NonlinearConstraint(constraint_function, lower_bounds, np.inf, jac='2-point')
result = minimize(lambda x: cost_function(x, positions), positions.flatten(), method='SLSQP',
jac="2-point", constraints=[nonlinear_constraint])
return result.x.reshape((-1, 2))
def _plot_bubbles(self, positions, sizes, colors, ax):
for (x, y), radius, color in zip(positions, sizes, colors):
ax.add_patch(plt.Circle((x, y), radius, color=color))
def _rescale_axis(self, ax):
ax.relim()
ax.autoscale_view()
ax.get_figure().canvas.draw()
def _plot_labels(self, positions, sizes, labels, ax, **font_kwargs):
font_kwargs.setdefault('horizontalalignment', 'center')
font_kwargs.setdefault('verticalalignment', 'center')
for (x, y), label, size in zip(positions, labels, sizes):
fontsize = _get_fontsize(size, label, ax, **font_kwargs)
ax.text(x, y, label, size=fontsize, **font_kwargs)
if __name__ == '__main__':
mapping = [("English", 25),
("French", 13),
("Spanish", 32),
("Thai", 10),
("Vietnamese", 13),
("Chinese", 20),
("Jamaican", 8),
("Scottish", 3),
("Irish", 12),
("American", 5),
("Romanian", 3),
("Dutch", 2)]
labels = [item[0] for item in mapping]
sizes = [item[1] for item in mapping]
colors = list(mcolors.XKCD_COLORS)
fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(aspect="equal"))
bc = BubbleChart(sizes, colors, labels, ax=ax)
ax.axis("off")
plt.show()
So what I'm trying to do is create a polar chart using plotly. However, it needs to look similar to a pie chart, where each label is given a slice of the circle. Currently the polar chart works fine, if I divide the circle into equal slices. But, when I try to give them a slice corresponding to the weights it doesn't work out too well, as it tends to overlap or leave spaces between each slice. This is mainly due to the Theta.
Can someone please explain where I've gone wrong?
Ratings - Max value is 5, Min value is 1. This is used to determine the length of the slice in the polar chart.
Weights - Max value is 100, Min value is 1. This is used to determine the width of the slice in the polar chart.
Labels - To identify each slice.
When equally splitting the circle
import plotly.graph_objects as go
import plotly.express as px
ratings = [3, 2, 5, 1, 2]
weights = [65, 79, 81, 98, 58]
labels = ["Strength", "Intelligence", "Dexterity", "Wisdom", "Stealth"]
def make_barpolar(ratings, weights, labels=None, colors=None, layout_options = None, **fig_kwargs):
# infer slice angles
num_slices = len(weights)
theta = [(i) * 360 / num_slices for i in range(0, num_slices)]
width = [360 / num_slices for _ in range(num_slices)]
# optionally infer colors
if colors is None:
color_seq = px.colors.qualitative.Safe
color_indices = range(0, len(color_seq), len(color_seq) // num_slices)
colors = [color_seq[i] for i in color_indices]
if layout_options is None:
layout_options = {}
if labels is None:
labels = ["" for _ in range(num_slices)]
layout_options["showlegend"] = False
# make figure
barpolar_plots = [go.Barpolar(r=[r], theta=[t], width=[w], name=n, marker_color=[c], **fig_kwargs)
for r, t, w, n, c in zip(ratings, theta, width, labels, colors)]
fig = go.Figure(barpolar_plots)
# additional layout parameters
fig.update_layout(**layout_options)
return fig
layout_options = {"title": "My Stats",
"title_font_size": 24,
"title_x": 0.5,
"legend_x": 0.85,
"legend_y": 0.5,
"polar_radialaxis_ticks": "",
"polar_radialaxis_showticklabels": False,
"polar_radialaxis_range": [0, max(ratings)],
"polar_angularaxis_ticks": "",
"polar_angularaxis_showticklabels": False}
fig = make_barpolar(ratings, weights, labels, layout_options=layout_options, opacity = 0.7)
fig.show()
Polar Chart 1
When using the weights to calculate the width and theta
import plotly.graph_objects as go
import plotly.express as px
ratings = [3, 2, 5, 1, 2]
weights = [65, 79, 81, 98, 38]
labels = ["Strength", "Intelligence", "Dexterity", "Wisdom", "Stealth"]
def make_barpolar(ratings, weights, labels=None, colors=None, layout_options = None, **fig_kwargs):
# infer slice angles
angles = [(weight / sum(weights) * 360) for weight in weights]
theta = []
num_slices = len(ratings)
theta = []
for index, angle in enumerate(angles):
if index < len(angles)-1:
if index == 0:
theta.append(0)
theta.append(theta[index] + angle)
width = angles
# optionally infer colors
if colors is None:
color_seq = px.colors.qualitative.Safe
color_indices = range(0, len(color_seq), len(color_seq) // num_slices)
colors = [color_seq[i] for i in color_indices]
if layout_options is None:
layout_options = {}
if labels is None:
labels = ["" for _ in range(num_slices)]
layout_options["showlegend"] = False
# make figure
barpolar_plots = [go.Barpolar(r=[r], theta=[t], width=[w], name=n, marker_color=[c], **fig_kwargs)
for r, t, w, n, c in zip(ratings, theta, width, labels, colors)]
fig = go.Figure(barpolar_plots)
# additional layout parameters
fig.update_layout(**layout_options)
return fig
layout_options = {"title": "My Stats",
"title_font_size": 24,
"title_x": 0.5,
"legend_x": 0.85,
"legend_y": 0.5,
"polar_radialaxis_ticks": "",
"polar_radialaxis_showticklabels": False,
"polar_radialaxis_range": [0, max(ratings)],
"polar_angularaxis_ticks": "",
"polar_angularaxis_showticklabels": False}
fig = make_barpolar(ratings, weights, labels, layout_options=layout_options, opacity = 0.7)
fig.show()
Polar Chart 2
I think you are assuming that theta sets the location of one edge of a radial sector when it is in fact the center of that radial sector. Here is your code but with a calculation of theta that accounts for this difference:
import plotly.graph_objects as go
import plotly.express as px
ratings = [3, 2, 5, 1, 2]
weights = [65, 79, 81, 98, 38]
labels = ["Strength", "Intelligence", "Dexterity", "Wisdom", "Stealth"]
def make_barpolar(ratings, weights, labels=None, colors=None, layout_options = None, **fig_kwargs):
# infer slice angles
angles = [(weight / sum(weights) * 360) for weight in weights]
num_slices = len(ratings)
theta = [0.5 * angle for angle in angles]
for index, angle in enumerate(angles):
for subsequent_index in range(index + 1, len(angles)):
theta[subsequent_index] += angle
width = angles
# optionally infer colors
if colors is None:
color_seq = px.colors.qualitative.Safe
color_indices = range(0, len(color_seq), len(color_seq) // num_slices)
colors = [color_seq[i] for i in color_indices]
if layout_options is None:
layout_options = {}
if labels is None:
labels = ["" for _ in range(num_slices)]
layout_options["showlegend"] = False
# make figure
barpolar_plots = [go.Barpolar(r=[r], theta=[t], width=[w], name=n, marker_color=[c], **fig_kwargs)
for r, t, w, n, c in zip(ratings, theta, width, labels, colors)]
fig = go.Figure(barpolar_plots)
# additional layout parameters
fig.update_layout(**layout_options)
return fig
layout_options = {"title": "My Stats",
"title_font_size": 24,
"title_x": 0.5,
"legend_x": 0.85,
"legend_y": 0.5,
"polar_radialaxis_ticks": "",
"polar_radialaxis_showticklabels": False,
"polar_radialaxis_range": [0, max(ratings)],
"polar_angularaxis_ticks": "",
"polar_angularaxis_showticklabels": False}
fig = make_barpolar(ratings, weights, labels, layout_options=layout_options, opacity = 0.7)
fig.show()
which gives
If you want to shift everything back so that the light blue sector is pointing directly to the right, you could always subtract 0.5 * angle[0] from every element of theta as one additional minor step.
__
P.S. Very high quality post for a first-time poster. Bravo!
Forgive the seemingly simple question.
I've got a radar chart I am using in Power BI that's pulling in data points and changing as I filter data in and out. One problem I am not able to solve is how I add numbers to the chart points. I got most of this code from another post on here, but I cannot seem to figure out how to add those data points.
Please see the picture examples if it doesn't make sense what I am looking for.
Current chart:
Desired chart:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, RegularPolygon
from matplotlib.path import Path
from matplotlib.projections.polar import PolarAxes
from matplotlib.projections import register_projection
from matplotlib.spines import Spine
from matplotlib.transforms import Affine2D
def radar_factory(num_vars, frame='circle'):
"""Create a radar chart with `num_vars` axes.
This function creates a RadarAxes projection and registers it.
Parameters
----------
num_vars : int
Number of variables for radar chart.
frame : {'circle' | 'polygon'}
Shape of frame surrounding axes.
"""
# calculate evenly-spaced axis angles
theta = np.linspace(0, 2*np.pi, num_vars, endpoint=False)
class RadarAxes(PolarAxes):
name = 'radar'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# rotate plot such that the first axis is at the top
self.set_theta_zero_location('N')
def fill(self, *args, closed=True, **kwargs):
"""Override fill so that line is closed by default"""
return super().fill(closed=closed, *args, **kwargs)
def plot(self, *args, **kwargs):
"""Override plot so that line is closed by default"""
lines = super().plot(19, **kwargs)
for line in lines:
self._close_line(line)
def _close_line(self, line):
x, y = line.get_data()
# FIXME: markers at x[0], y[0] get doubled-up
if x[0] != x[-1]:
x = np.concatenate((x, [x[0]]))
y = np.concatenate((y, [y[0]]))
line.set_data(x, y)
def set_varlabels(self, labels):
self.set_thetagrids(np.degrees(theta), labels)
def _gen_axes_patch(self):
# The Axes patch must be centered at (0.5, 0.5) and of radius 0.5
# in axes coordinates.
if frame == 'circle':
return Circle((0.5, 0.5), 0.5)
elif frame == 'polygon':
return RegularPolygon((0.5, 0.5), num_vars,
radius=0.5, edgecolor="k")
else:
raise ValueError("unknown value for 'frame': %s" % frame)
def draw(self, renderer):
""" Draw. If frame is polygon, make gridlines polygon-shaped """
if frame == 'polygon':
gridlines = self.yaxis.get_gridlines()
for gl in gridlines:
gl.get_path()._interpolation_steps = num_vars
super().draw(renderer)
def _gen_axes_spines(self):
if frame == 'circle':
return super()._gen_axes_spines()
elif frame == 'polygon':
# spine_type must be 'left'/'right'/'top'/'bottom'/'circle'.
spine = Spine(axes=self,
spine_type='circle',
path=Path.unit_regular_polygon(num_vars))
# unit_regular_polygon gives a polygon of radius 1 centered at
# (0, 0) but we want a polygon of radius 0.5 centered at (0.5,
# 0.5) in axes coordinates.
spine.set_transform(Affine2D().scale(.5).translate(.5, .5)
+ self.transAxes)
return {'polar': spine}
else:
raise ValueError("unknown value for 'frame': %s" % frame)
register_projection(RadarAxes)
return theta
disease_avg = round(dataset["disease_avg"].mean())
stress_vvg = round(dataset["Stress_Avg"].mean())
Nutrition_Avg = round(dataset["Nutrition_Avg"].mean())
Movement_avg = round(dataset["Movement_avg"].mean())
fitness_avg = round(dataset["fitness_avg"].mean())
data = [['Disease Risk', 'Stress', 'Nutrition', 'Movement Quality', 'Fitness'],
('Basecase', [[disease_avg, stress_vvg , Nutrition_Avg, Movement_avg, fitness_avg]])]
#('Basecase', [[dataset["disease_avg"].mean(), dataset["Stress_Avg"].mean() , dataset["Nutrition_Avg"].mean(),dataset["Movement_avg"].mean(), dataset["fitness_avg"].mean()]])]
cat = ['Disease Risk', 'Stress', 'Nutrition', 'Movement Quality', 'Fitness']
values = [dataset["disease_avg"].mean(), dataset["Stress_Avg"].mean(), dataset["Nutrition_Avg"].mean(), dataset["Movement_avg"].mean(), dataset["fitness_avg"].mean()]
N = len(data[0])
theta = radar_factory(N, frame='polygon')
spoke_labels = data.pop(0)
title, case_data = data[0]
fig, ax = plt.subplots(figsize=(4, 4), subplot_kw=dict(projection='radar'))
fig.subplots_adjust(wspace=0.25, hspace=0.20, top=0.85, bottom=0.05)
ax.set_rgrids([5, 10, 15, 20])
#ax.set_title(title, position=(0.5, 1.1), ha='center')
for d in case_data:
line = ax.plot(theta, d)
ax.fill(theta, d,color='#8bbe3f', alpha=0.35)
ax.set_varlabels(spoke_labels)
plt.show()
You could add the text labels during the loop where the filled polygon is plotted. Looping through the points of the polygon, ax.text(ti, di+1, 'text', ... puts a text at position (ti, di+1). Using di+1 puts the text just a little more outward than the polygon. Due to horizontal and vertical centering, all labels are positioned similarly.
Optionally, the points themselves could be marked, e.g. by calling scatter.
Like this:
for d in case_data:
line = ax.plot(theta, d)
ax.fill(theta, d, color='#8bbe3f', alpha=0.35)
for ti, di in zip(theta, d):
ax.text(ti, di+1, di, color='dodgerblue', ha='center', va='center')
ax.scatter(theta, d, color='crimson', s=10)
I tried with the package epade but I failed!
Example:
Each one of the x values defines the height of each bar (bars as many x values exist, with x height).
xa<-c(9.45,6.79,14.03,7.25,16.16,19.42,16.30,4.60,14.76,19.24,
16.04,7.80,13.16,10.00,15.76,16.29,19.52,27.22,7.74,6.75)
barplot(xa)
So I would like exactly the same plot in 3d looking fashion!
Is it possible?
UPDATED SOLUTION
This was done in Python, not in R :(
Here is the code:
# needed modules
import csv
import pandas as pandas
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from matplotlib import cm
from scipy.interpolate import spline
from textwrap import wrap
from mpl_toolkits.mplot3d import proj3d
import pylab
import os
# we define some names in order to change only one
# 3 columnes are imported each time
# by changing col_inc from 0 to something
# we can define which range of columns will be imported
col_num = np.arange(2, 1001)
col_num_tuple = tuple(col_num)
cnt = col_num_tuple
cnt
# last counter col_inc = 279
col_inc = 273
str = 0 + col_inc
fin = 3 + col_inc
cols = cnt[str:fin]
cols
# importing a simple datafile, csv type. Data are comma seperated
# importing only 1st, 2nd and 4th columns
# We can call these data now by giving a new name, 'io'.
io = pandas.read_csv(
'/data.csv', sep=",", usecols=cols)
# Try to get the name of singer & the name of song
# in the first two rows
names = io[0:2]
nm = names
nm1 = np.array(nm[:], dtype='string')
nm_singer = nm1[0:1][0:2, 1][0]
nm_song = nm1[1:2][0:2, 1][0]
nm_singer
nm_song
nms = nm_singer + ' - ' + nm_song
# we drop nan values
io = io.dropna()
# we make this in order not change each time, the name of column
io_all = np.array(io[3:])
io_st = np.array(io_all[:, 0], dtype=float)
io_end = np.array(io_all[:, 1], dtype=float)
io_dur = np.array(io_all[:, 2], dtype=float)
io_all
io_st
io_end
io_dur
# We define a new name for the column that is named alice inside io dataset
result = io_dur
# we need to make these data 'array type'
result = np.array(result)
# we now define the dimensions of our figure/plot, as well its dpi
fig = plt.figure(figsize=(16, 8), dpi=150)
# This line defines our first plot
# Afterwards, the '112' will define our second plot.
ax1 = fig.add_subplot(111, projection='3d')
# ax1 = Axes3D(fig)
# we define here labels
xlabels = io_end
xpos = np.arange(xlabels.shape[0])
ylabels = np.array([''])
ypos = np.arange(ylabels.shape[0])
xposM, yposM = np.meshgrid(xpos, ypos, copy=False)
zpos = result
zpos = zpos.ravel()
# this defines the dimensions of the actual boxes
# you can play with these values.
dx = 0.7
dy = 0.7
dz = zpos
# here, we define ticks, they are placed in the 'middle' of each bar
ax1.w_xaxis.set_ticks(xpos + dx / 2.)
ax1.w_xaxis.set_ticklabels(xlabels, rotation='vertical')
ax1.w_yaxis.set_ticks(ypos + dy / 2.)
ax1.w_yaxis.set_ticklabels(ylabels)
# here we define the colors of the bars, rainbow style
# you can play with these numbers
values = np.linspace(0.2, 1., xposM.ravel().shape[0])
colors = cm.rainbow(values)
# figure subtitle
# fig.suptitle('test title', fontsize=20)
# here, we define in the x axis the size of its ticks, its numbers
ax1.tick_params(axis='x', which='major', pad=0, labelsize=7)
# Here, we define the limits of y axis,
# NOTE that this defines WHERE bars will be placed
# IN relation to the rest figure,
# their offset point
plt.ylim((-2, 5))
# this says if the grid will be printed
plt.grid(True)
# this defines the placement of the 3d plot in its placeholders,
# in the surrounding white space
# I was surprised! The below line is not needed at all!
# fig.subplots_adjust(left=0, right=0, bottom=0, top=0)
# this is the actual command to define the plot
# all 6 parameters that we previously defined, are placed here.
# colors is an extra parameter
ax1.bar3d(xposM.ravel(), yposM.ravel(), dz * 0, dx, dy, dz, color=colors)
# elevation and azimuth, basically, definition of the view angle
ax1.view_init(0, -95)
# here we define that we will place a second plot
# Neither this line is needed!
# ax1 = fig.add_subplot(112, projection='3d')
# To produce numbers from 0 according to how many data exist in 'result'
x = np.arange(0, len(result))
y = result
# I try to center the line in relation to the top of bars.
y += 5
# Produce more points in order to make the line to look nicer (300).
x_smooth = np.linspace(x.min(), x.max(), 300)
y_smooth = spline(x, y, x_smooth)
# smooth line sometimes went below zero in some extreme cases.
# Therefore I added this if statement to find these cases
# and increase the height of the smooth line so much points
# as the points that went below 0
if min(y_smooth) <= 0:
y -= (min(y_smooth))-1
y_smooth = spline(x, y, x_smooth)
# a trick to center the line to bars
x_smooth += 0.4
# here,i try to produce a 'z' array of so many zeros as the length
# of 'x_smooth line'
z = np.linspace(0, 0, len(x_smooth))
# here, we define the parameters of the second plot.
# ax1' symbol is duplicated
# in order to plot the line in the same plot with the barplot.
ax1.plot(x_smooth, z, y_smooth)
# this try to align the y title
ax1.annotate(
'\n'.join(wrap('Duration of each Rythm (in sec)', 20)),
xy=(0.20, 0.80), xytext=(0, 0), fontsize=8, color='steelblue',
style='italic',
xycoords='axes fraction', textcoords='offset points',
bbox=dict(facecolor='mistyrose', alpha=0.3),
horizontalalignment='center', verticalalignment='down')
# this try to align the x title
ax1.annotate(
'\n'.join(wrap('Where Rythm is broken (in sec)', 20)),
xy=(0.27, 0.06), xytext=(0, 0), fontsize=9, color='steelblue',
xycoords='axes fraction', textcoords='offset points',
bbox=dict(facecolor='peachpuff', alpha=0.3),
horizontalalignment='center', verticalalignment='down')
# this try to center the bottom title
ax1.annotate(
'\n'.join(wrap(nms, 100)), xy=(0.5, 0.07),
xytext=(0, 0), fontsize=11,
xycoords='axes fraction', textcoords='offset points',
bbox=dict(facecolor='mediumorchid', alpha=0.3),
horizontalalignment='center', verticalalignment='down')
# Eedefine path and filename in order to save in custom made filename
pathnm = '/'
filenm = nms
nflnm = '%s_3D.png' % filenm
npath = os.path.join(pathnm, nflnm)
# saving our plot
#fig.savefig(npath, bbox_inches='tight', pad_inches=0,figsize=(46,15),dpi=400)
plt.show(fig)
io[0:2]'code'
I have a signal with sampling rate 16e3, its frequency range is from 125 to 1000 Hz.
So if i plot a specgram i get a pretty small colorrange because of all the unused frequencys.
ive tried to fix it with setting ax limits but that does not work.
is there any way to cut off unused frequencys or replace them with NaNs?
Resampling the Data to 2e3 won't work because there are still some not used frequencys below 125 Hz.
Thanks for your help.
specgram() is doing all the work for you. If you look in axes.py at the specgram function you can see how it works. The original function is in Python27\Lib\site-packages\matplotlib\axes.py on my computer.
<snip>
Pxx, freqs, bins = mlab.specgram(x, NFFT, Fs, detrend,
window, noverlap, pad_to, sides, scale_by_freq)
Z = 10. * np.log10(Pxx)
Z = np.flipud(Z)
if xextent is None: xextent = 0, np.amax(bins)
xmin, xmax = xextent
freqs += Fc
extent = xmin, xmax, freqs[0], freqs[-1]
im = self.imshow(Z, cmap, extent=extent, **kwargs)
self.axis('auto')
return Pxx, freqs, bins, im
You might have to make your own function modeled on this and clip the Pxx data to suit your needs.
Pxx, freqs, bins = mlab.specgram(x, NFFT, Fs, detrend,
window, noverlap, pad_to, sides, scale_by_freq)
# ****************
# create a new limited Pxx and freqs
#
# ****************
Z = 10. * np.log10(Pxx)
Z = np.flipud(Z)
Pxx is a 2d array with a shape of (len(freqs),len(bins)
>>> Pxx.shape
(129, 311)
>>> freqs.shape
(129,)
>>> bins.shape
(311,)
>>>
This will limit Pxx and freqs
Pxx = Pxx[(freqs >= 125) & (freqs <= 1000)]
freqs = freqs[(freqs >= 125) & (freqs <= 1000)]
Here is a complete solution - my_specgram() - used with the specgram_demo from the gallery.
from pylab import *
from matplotlib import *
# 100, 400 and 200 Hz sine 'wave'
dt = 0.0005
t = arange(0.0, 20.0, dt)
s1 = sin(2*pi*100*t)
s2 = 2*sin(2*pi*400*t)
s3 = 2*sin(2*pi*200*t)
# create a transient "chirp"
mask = where(logical_and(t>10, t<12), 1.0, 0.0)
s2 = s2 * mask
# add some noise into the mix
nse = 0.01*randn(len(t))
x = s1 + s2 + +s3 + nse # the signal
NFFT = 1024 # the length of the windowing segments
Fs = int(1.0/dt) # the sampling frequency
# modified specgram()
def my_specgram(x, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none,
window=mlab.window_hanning, noverlap=128,
cmap=None, xextent=None, pad_to=None, sides='default',
scale_by_freq=None, minfreq = None, maxfreq = None, **kwargs):
"""
call signature::
specgram(x, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none,
window=mlab.window_hanning, noverlap=128,
cmap=None, xextent=None, pad_to=None, sides='default',
scale_by_freq=None, minfreq = None, maxfreq = None, **kwargs)
Compute a spectrogram of data in *x*. Data are split into
*NFFT* length segments and the PSD of each section is
computed. The windowing function *window* is applied to each
segment, and the amount of overlap of each segment is
specified with *noverlap*.
%(PSD)s
*Fc*: integer
The center frequency of *x* (defaults to 0), which offsets
the y extents of the plot to reflect the frequency range used
when a signal is acquired and then filtered and downsampled to
baseband.
*cmap*:
A :class:`matplotlib.cm.Colormap` instance; if *None* use
default determined by rc
*xextent*:
The image extent along the x-axis. xextent = (xmin,xmax)
The default is (0,max(bins)), where bins is the return
value from :func:`mlab.specgram`
*minfreq, maxfreq*
Limits y-axis. Both required
*kwargs*:
Additional kwargs are passed on to imshow which makes the
specgram image
Return value is (*Pxx*, *freqs*, *bins*, *im*):
- *bins* are the time points the spectrogram is calculated over
- *freqs* is an array of frequencies
- *Pxx* is a len(times) x len(freqs) array of power
- *im* is a :class:`matplotlib.image.AxesImage` instance
Note: If *x* is real (i.e. non-complex), only the positive
spectrum is shown. If *x* is complex, both positive and
negative parts of the spectrum are shown. This can be
overridden using the *sides* keyword argument.
**Example:**
.. plot:: mpl_examples/pylab_examples/specgram_demo.py
"""
#####################################
# modified axes.specgram() to limit
# the frequencies plotted
#####################################
# this will fail if there isn't a current axis in the global scope
ax = gca()
Pxx, freqs, bins = mlab.specgram(x, NFFT, Fs, detrend,
window, noverlap, pad_to, sides, scale_by_freq)
# modified here
#####################################
if minfreq is not None and maxfreq is not None:
Pxx = Pxx[(freqs >= minfreq) & (freqs <= maxfreq)]
freqs = freqs[(freqs >= minfreq) & (freqs <= maxfreq)]
#####################################
Z = 10. * np.log10(Pxx)
Z = np.flipud(Z)
if xextent is None: xextent = 0, np.amax(bins)
xmin, xmax = xextent
freqs += Fc
extent = xmin, xmax, freqs[0], freqs[-1]
im = ax.imshow(Z, cmap, extent=extent, **kwargs)
ax.axis('auto')
return Pxx, freqs, bins, im
# plot
ax1 = subplot(211)
plot(t, x)
subplot(212, sharex=ax1)
# the minfreq and maxfreq args will limit the frequencies
Pxx, freqs, bins, im = my_specgram(x, NFFT=NFFT, Fs=Fs, noverlap=900,
cmap=cm.Accent, minfreq = 180, maxfreq = 220)
show()
close()
These days, there's an easier way to do this than when the question was asked: you can use matplotlib.pyplot.axis to set ymin and ymax to your desired frequencies. It's quite easy; here's a snippet from my program:
plt.specgram(xmit, NFFT=65536, Fs=Fs)
plt.axis(ymin=Fc-Fa*10, ymax=Fc+Fa*10)
plt.show()
specgram() returns (Pxx, freqs, bins, im), where im is AxesImage
instance [1]. You could use it to set the limits of your plot:
Pxx, freqs, bins, im = plt.specgram(signal, Fs=fs)
im.set_ylim((125,1000))
[1] http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.specgram
Here is an adapted version of this: http://matplotlib.org/examples/pylab_examples/specgram_demo.html
which changes the range of frequencies that are plotted.
#!/usr/bin/env python
#### from the example
####
from pylab import *
dt = 0.0005
t = arange(0.0, 20.0, dt)
s1 = sin(2*pi*100*t)
s2 = 2*sin(2*pi*400*t)
mask = where(logical_and(t>10, t<12), 1.0, 0.0)
s2 = s2 * mask
nse = 0.01*randn(len(t))
x = s1 + s2 + nse # the signal
NFFT = 1024 # the length of the windowing segments
Fs = int(1.0/dt) # the sampling frequency
ax1 = subplot(211)
plot(t, x)
subplot(212, sharex=ax1)
Pxx, freqs, bins, im = specgram(x, NFFT=NFFT, Fs=Fs, noverlap=900,
cmap=cm.gist_heat)
#### edited from the example
####
# here we get access to the axes
x1,x2,y1,y2 = axis()
# leave x range the same, change y (frequency) range
axis((x1,x2,25,500))
show()