Package sabx10 :: Package profiles :: Module plotter
[hide private]
[frames] | no frames]

Source Code for Module sabx10.profiles.plotter

  1  ############################################################################### 
  2  # 
  3  # sabx10 - an SABX file manipulation library 
  4  # Copyright (C) 2009, 2010 Jay Farrimond (jay@sabikerides.com) 
  5  # 
  6  # This file is part of sabx10. 
  7  # 
  8  # sabx10 is free software: you can redistribute it and/or modify it under the 
  9  # terms of the GNU General Public License as published by the Free Software 
 10  # Foundation, either version 3 of the License, or (at your option) any later 
 11  # version. 
 12  # 
 13  # sabx10 is distributed in the hope that it will be useful, but WITHOUT ANY 
 14  # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 
 15  # A PARTICULAR PURPOSE.  See the GNU General Public License for more details. 
 16  # 
 17  # You should have received a copy of the GNU General Public License along with 
 18  # sabx10.  If not, see <http://www.gnu.org/licenses/>. 
 19  # 
 20  ############################################################################### 
 21  """ 
 22  Handle the plotting of profile data and saving the profile to a file. 
 23  """ 
 24  import math 
 25  import os.path 
 26   
 27  import matplotlib 
 28  matplotlib.use('Agg') 
 29  import matplotlib.pyplot as plt 
 30   
 31  from sabx10.oxm import mile_feet 
 32   
 33  from consts import SMALL_WIDTH, SMALL_HEIGHT, LARGE_WIDTH, LARGE_HEIGHT, \ 
 34      FONT_POINTS_PER_INCH, FONT_NAME, LABEL_FONT_SIZE, ANNO_FONT_SIZE 
 35   
36 -def _poly_gen(distances, elevations):
37 """ 38 Generator to provide the plot fill data for a graph based on a list of 39 distances (X axis) and elevations (Y axis). Fill data is a grade-dependant 40 color and x and y co-ordinates needed to specify a polygon on the graph so 41 it can be filled in with the proper color. 42 43 @param distances: C{list} of distances for points in segment being plotted 44 @type distances: C{list} of C{float} 45 @param elevations: C{list} of elevations for point in segment being plotted 46 @type elevations: C{list} of C{float} 47 48 @return: (x1, x2), (y1, y2), color 49 @rtype: (C{float},C{float}), (C{float},C{float}), C{string} 50 """ 51 colors = {0: '1.0', 52 1: '0.9', 53 2: '0.8', 54 3: '0.7', 55 4: '0.6', 56 5: '0.5', 57 6: '0.4', 58 7: '0.3', 59 8: '0.2', 60 9: '0.1', 61 10: '0.0'} 62 63 for x in range(len(distances)-1): 64 if distances[x+1]-distances[x] == 0: 65 grade = 0; 66 else: 67 grade = ((elevations[x+1]-elevations[x])/ 68 ((distances[x+1]-distances[x]) * mile_feet)) * 100.0 69 grade = math.trunc(grade) 70 grade = max(0, grade) 71 grade = min(10, grade) 72 73 yield (distances[x], distances[x+1]), \ 74 (elevations[x], elevations[x+1]), \ 75 colors[grade]
76
77 -class ProfilePlotter(object):
78 """ 79 Generates a profile file. 80 81 @ivar ride: L{Ride} to process 82 @type ride: L{Ride} 83 @ivar graph_filebase: base name for profile files 84 @type graph_filebase: C{string} 85 @ivar graph_dir: directory to write profile files into 86 @type graph_dir: C{string} 87 @ivar dpi: resolution of profile file 88 @type dpi: C{int} 89 @ivar min_anno_dist: minimum distance between annotations 90 @type min_anno_dist: C{float} 91 @ivar longest: length of longest segment 92 @type longest: C{float} 93 @ivar lowest: lowest elevation 94 @type lowest: C{float} 95 @ivar highest: highest elevation 96 @type highest: C{float} 97 """ 98 ### initialize ###
99 - def _calc_most_least(self):
100 """ 101 Go through the segments for the ride and find the longest segment, the 102 lowest elevation, and the highest elevation. 103 """ 104 self.longest = 0.0 105 self.lowest = 20000.0 106 self.highest = 0.0 107 108 for seg in self.ride.segs: 109 seg_lowest, seg_highest = seg.find_lowest_highest() 110 self.lowest = min(self.lowest, seg_lowest) 111 self.highest = max(self.highest, seg_highest) 112 self.longest = max(self.longest, seg.length)
113
114 - def __init__(self, ride, graph_filebase, graph_dir, dpi):
115 """ 116 Save the passed-in data and generate calculated instance data. 117 118 @param ride: L{Ride} to process 119 @type ride: L{Ride} 120 @param graph_filebase: base name for profile files 121 @type graph_filebase: C{string} 122 @param graph_dir: directory to write profile files into 123 @type graph_dir: C{string} 124 @param dpi: resolution of profile file 125 @type dpi: C{int} 126 """ 127 self.ride = ride 128 self.graph_filebase = graph_filebase 129 self.graph_dir = graph_dir 130 self.dpi = dpi 131 self.min_anno_dist = self.ride.distance / 50.0 132 self._calc_most_least()
133
134 - def _set_min_anno_dist(self, inch_width):
135 """ 136 Calculate and set the min_ann_dist for the plot. This is how far apart 137 annotations need to be before they overlap. 138 139 @param inch_width: width of profile, in inches 140 @type inch_width: C{float} 141 """ 142 plot_inch_width = inch_width * 0.7 143 label_inch_width = (float(ANNO_FONT_SIZE) / float(FONT_POINTS_PER_INCH)) 144 miles_per_inch = self.ride.distance / plot_inch_width 145 miles_per_label = miles_per_inch * label_inch_width 146 self.min_anno_dist = miles_per_label * 1.1
147
148 - def _normalize_elevation(self, elevation):
149 """ 150 Normalize an elevation in relation to the lowest elevation in the ride, 151 such that the lowest elevation in the ride will be at an elevation of 152 zero. 153 154 @param elevation: elevation to normalize 155 @type elevation: C{float} 156 157 @return: normalized elevation 158 @rtype: C{float} 159 """ 160 return elevation - self.lowest
161
162 - def _normalize_elevations(self, elevations):
163 """ 164 Normalize all the elevations in the list. 165 166 @param elevations: C{list} of elevations to normalize 167 @type elevations: C{list} of C{float} 168 """ 169 return [self._normalize_elevation(ele) for ele in elevations]
170
171 - def _plot_graph(self, distances, elevations, length):
172 """ 173 Plot the graph of the distances and elevations. Fill-in under the 174 graph based on the grade between points. 175 176 @param distances: C{list} of distances to plot 177 @type distances: C{list} of C{float} 178 @param elevations: C{list} of elevations corresponding to the distances 179 @type elevations: C{list} of C{float} 180 @param length: length of set of points being plotted 181 @type length: C{float} 182 """ 183 elevations = self._normalize_elevations(elevations) 184 for x, y, color in _poly_gen(distances, elevations): 185 plt.fill_between(x, y, color=color, edgecolor=color) 186 plt.plot(distances, elevations, scalex=False, scaley=False) 187 plt.axis([0.0, max(length, 1.0), 0.0, self.highest - self.lowest])
188
189 - def _filter_annotations(self, annotations):
190 """ 191 Take the annotation list and remove annotations that are too close to 192 the ones next to it, to prevent overlap. 193 194 @param annotations: list of annotations to filter 195 @type annotations: C{list} of L{Annotation} 196 """ 197 prev_dist = 0.0 198 ann_dist = 0.0 199 filtered = [] 200 201 for anno in annotations: 202 ann_dist += anno.dist - prev_dist 203 prev_dist = anno.dist 204 if ann_dist >= self.min_anno_dist: 205 filtered.append(anno) 206 ann_dist = 0.0 207 208 return filtered
209
210 - def _normalize_annotations(self, annotations):
211 """ 212 Normalize the elevations of the annotation points. 213 214 @param annotations: list of annotations to filter 215 @type annotations: C{list} of L{Annotation} 216 217 @return: list of filtered annotations 218 @rtype: C{list} of L{Annotation} 219 """ 220 for anno in annotations: 221 anno.ele = self._normalize_elevation(anno.ele) 222 return annotations
223
224 - def _plot_annotations(self, annotations):
225 """ 226 Add the annotations to the current plot (if there are any). Filter and 227 normalize them first. 228 229 @param annotations: list of annotations to filter 230 @type annotations: C{list} of L{Annotation} 231 """ 232 if annotations is None: 233 return 234 235 annotations = self._filter_annotations(annotations) 236 annotations = self._normalize_annotations(annotations) 237 for anno in annotations: 238 plt.annotate(anno.text, (anno.dist, anno.ele), xytext=(-4,20), 239 textcoords='offset points', 240 arrowprops=dict(arrowstyle="->", relpos=(0.5,0.0)), 241 rotation='vertical', 242 fontname=FONT_NAME, fontsize=ANNO_FONT_SIZE)
243
244 - def _plot_profile(self, distances, elevations, length, annotations):
245 """ 246 Graph the distances, elevations, and annotations on the current plot. 247 248 @param distances: list of distances for the plot 249 @type distances: C{list} of C{float} 250 @param elevations: list of elevations for the plot 251 @type elevations: C{list} of C{float} 252 @param length: overall length of plot 253 @type length: C{float} 254 @param annotations: annotations for the plot 255 @type annotations: C{list} of L{Annotation} 256 """ 257 plt.grid(False) 258 self._plot_graph(distances, elevations, length) 259 self._plot_annotations(annotations)
260
261 - def _save_profile(self, seg_index, size_name):
262 """ 263 Save the current plot to a file. 264 265 @param seg_index: index of segment being plotted 266 @type seg_index: C{int} 267 @param size_name: size name being plotted 268 @type size_name: C{string} 269 """ 270 file_name = '%s_prof_%s_%s_%s.png' % (self.graph_filebase, size_name, 271 self.ride.index, seg_index) 272 plt.savefig(os.path.join(self.graph_dir, file_name), dpi=self.dpi) 273 plt.close()
274
275 - def plot_small_profile(self, distances, elevations, length, 276 seg_index='all', annotations=None):
277 """ 278 Plot a small sized profile. 279 280 @param distances: list of distances for the plot 281 @type distances: C{list} of C{float} 282 @param elevations: list of elevations for the plot 283 @type elevations: C{list} of C{float} 284 @param length: overall length of plot 285 @type length: C{float} 286 @param seg_index: index of segment being plotted 287 @type seg_index: C{int} 288 @param annotations: annotations for the plot 289 @type annotations: C{list} of L{Annotation} 290 """ 291 self._set_min_anno_dist(SMALL_WIDTH) 292 plt.figure(1, figsize=(SMALL_WIDTH, SMALL_HEIGHT)) 293 294 self._plot_profile(distances, elevations, length, annotations) 295 self._save_profile(seg_index, 'small')
296
297 - def plot_large_profile(self, distances, elevations, length, 298 seg_index='all', annotations=None):
299 """ 300 Plot a large sized profile. 301 302 @param distances: list of distances for the plot 303 @type distances: C{list} of C{float} 304 @param elevations: list of elevations for the plot 305 @type elevations: C{list} of C{float} 306 @param length: overall length of plot 307 @type length: C{float} 308 @param seg_index: index of segment being plotted 309 @type seg_index: C{int} 310 @param annotations: annotations for the plot 311 @type annotations: C{list} of L{Annotation} 312 """ 313 self._set_min_anno_dist(LARGE_WIDTH) 314 plt.figure(1, figsize=(LARGE_WIDTH, LARGE_HEIGHT)) 315 plt.xlabel('Distance (miles)', fontname=FONT_NAME, fontsize=LABEL_FONT_SIZE) 316 plt.ylabel('Elevation (feet)', fontname=FONT_NAME, fontsize=LABEL_FONT_SIZE) 317 if annotations is None: 318 plt.title('Profile', fontname=FONT_NAME, fontsize=LABEL_FONT_SIZE) 319 else: 320 plt.box(False) 321 plt.axhline(y=0.001, color='black') 322 plt.axvline(color='black') 323 324 self._plot_profile(distances, elevations, length, annotations) 325 self._save_profile(seg_index, 'large')
326