1#Copyright ReportLab Europe Ltd. 2000-2017 2#see license.txt for license details 3#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/utils.py 4 5__version__='3.3.0' 6__doc__="Utilities used here and there." 7from time import mktime, gmtime, strftime 8from math import log10, pi, floor, sin, cos, sqrt, hypot 9import weakref 10from reportlab.graphics.shapes import transformPoint, transformPoints, inverse, Ellipse, Group, String, Path, numericXShift 11from reportlab.lib.utils import flatten 12from reportlab.pdfbase.pdfmetrics import stringWidth 13 14### Dinu's stuff used in some line plots (likely to vansih). 15def mkTimeTuple(timeString): 16 "Convert a 'dd/mm/yyyy' formatted string to a tuple for use in the time module." 17 18 L = [0] * 9 19 dd, mm, yyyy = list(map(int, timeString.split('/'))) 20 L[:3] = [yyyy, mm, dd] 21 22 return tuple(L) 23 24def str2seconds(timeString): 25 "Convert a number of seconds since the epoch into a date string." 26 27 return mktime(mkTimeTuple(timeString)) 28 29def seconds2str(seconds): 30 "Convert a date string into the number of seconds since the epoch." 31 32 return strftime('%Y-%m-%d', gmtime(seconds)) 33 34### Aaron's rounding function for making nice values on axes. 35def nextRoundNumber(x): 36 """Return the first 'nice round number' greater than or equal to x 37 38 Used in selecting apropriate tick mark intervals; we say we want 39 an interval which places ticks at least 10 points apart, work out 40 what that is in chart space, and ask for the nextRoundNumber(). 41 Tries the series 1,2,5,10,20,50,100.., going up or down as needed. 42 """ 43 44 #guess to nearest order of magnitude 45 if x in (0, 1): 46 return x 47 48 if x < 0: 49 return -1.0 * nextRoundNumber(-x) 50 else: 51 lg = int(log10(x)) 52 53 if lg == 0: 54 if x < 1: 55 base = 0.1 56 else: 57 base = 1.0 58 elif lg < 0: 59 base = 10.0 ** (lg - 1) 60 else: 61 base = 10.0 ** lg # e.g. base(153) = 100 62 # base will always be lower than x 63 64 if base >= x: 65 return base * 1.0 66 elif (base * 2) >= x: 67 return base * 2.0 68 elif (base * 5) >= x: 69 return base * 5.0 70 else: 71 return base * 10.0 72 73_intervals=(.1, .2, .25, .5) 74_j_max=len(_intervals)-1 75def find_interval(lo,hi,I=5): 76 'determine tick parameters for range [lo, hi] using I intervals' 77 78 if lo >= hi: 79 if lo==hi: 80 if lo==0: 81 lo = -.1 82 hi = .1 83 else: 84 lo = 0.9*lo 85 hi = 1.1*hi 86 else: 87 raise ValueError("lo>hi") 88 x=(hi - lo)/float(I) 89 b= (x>0 and (x<1 or x>10)) and 10**floor(log10(x)) or 1 90 b = b 91 while 1: 92 a = x/b 93 if a<=_intervals[-1]: break 94 b = b*10 95 96 j = 0 97 while a>_intervals[j]: j = j + 1 98 99 while 1: 100 ss = _intervals[j]*b 101 n = lo/ss 102 l = int(n)-(n<0) 103 n = ss*l 104 x = ss*(l+I) 105 a = I*ss 106 if n>0: 107 if a>=hi: 108 n = 0.0 109 x = a 110 elif hi<0: 111 a = -a 112 if lo>a: 113 n = a 114 x = 0 115 if hi<=x and n<=lo: break 116 j = j + 1 117 if j>_j_max: 118 j = 0 119 b = b*10 120 return n, x, ss, lo - n + x - hi 121 122def find_good_grid(lower,upper,n=(4,5,6,7,8,9), grid=None): 123 if grid: 124 t = divmod(lower,grid)[0] * grid 125 hi, z = divmod(upper,grid) 126 if z>1e-8: hi = hi+1 127 hi = hi*grid 128 else: 129 try: 130 n[0] 131 except TypeError: 132 n = range(max(1,n-2),max(n+3,2)) 133 134 w = 1e308 135 for i in n: 136 z=find_interval(lower,upper,i) 137 if z[3]<w: 138 t, hi, grid = z[:3] 139 w=z[3] 140 return t, hi, grid 141 142def ticks(lower, upper, n=(4,5,6,7,8,9), split=1, percent=0, grid=None, labelVOffset=0): 143 ''' 144 return tick positions and labels for range lower<=x<=upper 145 n=number of intervals to try (can be a list or sequence) 146 split=1 return ticks then labels else (tick,label) pairs 147 ''' 148 t, hi, grid = find_good_grid(lower, upper, n, grid) 149 power = floor(log10(grid)) 150 if power==0: power = 1 151 w = grid/10.**power 152 w = int(w)!=w 153 154 if power > 3 or power < -3: 155 format = '%+'+repr(w+7)+'.0e' 156 else: 157 if power >= 0: 158 digits = int(power)+w 159 format = '%' + repr(digits)+'.0f' 160 else: 161 digits = w-int(power) 162 format = '%'+repr(digits+2)+'.'+repr(digits)+'f' 163 164 if percent: format=format+'%%' 165 T = [] 166 n = int(float(hi-t)/grid+0.1)+1 167 if split: 168 labels = [] 169 for i in range(n): 170 v = t+grid*i 171 T.append(v) 172 labels.append(format % (v+labelVOffset)) 173 return T, labels 174 else: 175 for i in range(n): 176 v = t+grid*i 177 T.append((v, format % (v+labelVOffset))) 178 return T 179 180def findNones(data): 181 m = len(data) 182 if None in data: 183 b = 0 184 while b<m and data[b] is None: 185 b += 1 186 if b==m: return data 187 l = m-1 188 while data[l] is None: 189 l -= 1 190 l+=1 191 if b or l: data = data[b:l] 192 I = [i for i in range(len(data)) if data[i] is None] 193 for i in I: 194 data[i] = 0.5*(data[i-1]+data[i+1]) 195 return b, l, data 196 return 0,m,data 197 198def pairFixNones(pairs): 199 Y = [x[1] for x in pairs] 200 b,l,nY = findNones(Y) 201 m = len(Y) 202 if b or l<m or nY!=Y: 203 if b or l<m: pairs = pairs[b:l] 204 pairs = [(x[0],y) for x,y in zip(pairs,nY)] 205 return pairs 206 207def maverage(data,n=6): 208 data = (n-1)*[data[0]]+data 209 data = [float(sum(data[i-n:i]))/n for i in range(n,len(data)+1)] 210 return data 211 212def pairMaverage(data,n=6): 213 return [(x[0],s) for x,s in zip(data, maverage([x[1] for x in data],n))] 214 215class DrawTimeCollector(object): 216 ''' 217 generic mechanism for collecting information about nodes at the time they are about to be drawn 218 ''' 219 def __init__(self,formats=['gif']): 220 self._nodes = weakref.WeakKeyDictionary() 221 self.clear() 222 self._pmcanv = None 223 self.formats = formats 224 self.disabled = False 225 226 def clear(self): 227 self._info = [] 228 self._info_append = self._info.append 229 230 def record(self,func,node,*args,**kwds): 231 self._nodes[node] = (func,args,kwds) 232 node.__dict__['_drawTimeCallback'] = self 233 234 def __call__(self,node,canvas,renderer): 235 func = self._nodes.get(node,None) 236 if func: 237 func, args, kwds = func 238 i = func(node,canvas,renderer, *args, **kwds) 239 if i is not None: self._info_append(i) 240 241 @staticmethod 242 def rectDrawTimeCallback(node,canvas,renderer,**kwds): 243 A = getattr(canvas,'ctm',None) 244 if not A: return 245 x1 = node.x 246 y1 = node.y 247 x2 = x1 + node.width 248 y2 = y1 + node.height 249 250 D = kwds.copy() 251 D['rect']=DrawTimeCollector.transformAndFlatten(A,((x1,y1),(x2,y2))) 252 return D 253 254 @staticmethod 255 def transformAndFlatten(A,p): 256 ''' transform an flatten a list of points 257 A transformation matrix 258 p points [(x0,y0),....(xk,yk).....] 259 ''' 260 if tuple(A)!=(1,0,0,1,0,0): 261 iA = inverse(A) 262 p = transformPoints(iA,p) 263 return tuple(flatten(p)) 264 265 @property 266 def pmcanv(self): 267 if not self._pmcanv: 268 import renderPM 269 self._pmcanv = renderPM.PMCanvas(1,1) 270 return self._pmcanv 271 272 def wedgeDrawTimeCallback(self,node,canvas,renderer,**kwds): 273 A = getattr(canvas,'ctm',None) 274 if not A: return 275 if isinstance(node,Ellipse): 276 c = self.pmcanv 277 c.ellipse(node.cx, node.cy, node.rx,node.ry) 278 p = c.vpath 279 p = [(x[1],x[2]) for x in p] 280 else: 281 p = node.asPolygon().points 282 p = [(p[i],p[i+1]) for i in range(0,len(p),2)] 283 284 D = kwds.copy() 285 D['poly'] = self.transformAndFlatten(A,p) 286 return D 287 288 def save(self,fnroot): 289 ''' 290 save the current information known to this collector 291 fnroot is the root name of a resource to name the saved info 292 override this to get the right semantics for your collector 293 ''' 294 import pprint 295 f=open(fnroot+'.default-collector.out','w') 296 try: 297 pprint.pprint(self._info,f) 298 finally: 299 f.close() 300 301def xyDist(xxx_todo_changeme, xxx_todo_changeme1 ): 302 '''return distance between two points''' 303 (x0,y0) = xxx_todo_changeme 304 (x1,y1) = xxx_todo_changeme1 305 return hypot((x1-x0),(y1-y0)) 306 307def lineSegmentIntersect(xxx_todo_changeme2, xxx_todo_changeme3, xxx_todo_changeme4, xxx_todo_changeme5 308 ): 309 (x00,y00) = xxx_todo_changeme2 310 (x01,y01) = xxx_todo_changeme3 311 (x10,y10) = xxx_todo_changeme4 312 (x11,y11) = xxx_todo_changeme5 313 p = x00,y00 314 r = x01-x00,y01-y00 315 316 317 q = x10,y10 318 s = x11-x10,y11-y10 319 320 rs = float(r[0]*s[1]-r[1]*s[0]) 321 qp = q[0]-p[0],q[1]-p[1] 322 323 qpr = qp[0]*r[1]-qp[1]*r[0] 324 qps = qp[0]*s[1]-qp[1]*s[0] 325 326 if abs(rs)<1e-8: 327 if abs(qpr)<1e-8: return 'collinear' 328 return None 329 330 t = qps/rs 331 u = qpr/rs 332 333 if 0<=t<=1 and 0<=u<=1: 334 return p[0]+t*r[0], p[1]+t*r[1] 335 336def makeCircularString(x, y, radius, angle, text, fontName, fontSize, inside=0, G=None,textAnchor='start'): 337 '''make a group with circular text in it''' 338 if not G: G = Group() 339 340 angle %= 360 341 pi180 = pi/180 342 phi = angle*pi180 343 width = stringWidth(text, fontName, fontSize) 344 sig = inside and -1 or 1 345 hsig = sig*0.5 346 sig90 = sig*90 347 348 if textAnchor!='start': 349 if textAnchor=='middle': 350 phi += sig*(0.5*width)/radius 351 elif textAnchor=='end': 352 phi += sig*float(width)/radius 353 elif textAnchor=='numeric': 354 phi += sig*float(numericXShift(textAnchor,text,width,fontName,fontSize,None))/radius 355 356 for letter in text: 357 width = stringWidth(letter, fontName, fontSize) 358 beta = float(width)/radius 359 h = Group() 360 h.add(String(0, 0, letter, fontName=fontName,fontSize=fontSize,textAnchor="start")) 361 h.translate(x+cos(phi)*radius,y+sin(phi)*radius) #translate to radius and angle 362 h.rotate((phi-hsig*beta)/pi180-sig90) # rotate as needed 363 G.add(h) #add to main group 364 phi -= sig*beta #increment 365 366 return G 367 368class CustomDrawChanger: 369 ''' 370 a class to simplify making changes at draw time 371 ''' 372 def __init__(self): 373 self.store = None 374 375 def __call__(self,change,obj): 376 if change: 377 self.store = self._changer(obj) 378 assert isinstance(self.store,dict), '%s.changer should return a dict of changed attributes' % self.__class__.__name__ 379 elif self.store is not None: 380 for a,v in self.store.items(): 381 setattr(obj,a,v) 382 self.store = None 383 384 def _changer(self,obj): 385 ''' 386 When implemented this method should return a dictionary of 387 original attribute values so that a future self(False,obj) 388 can restore them. 389 ''' 390 raise RuntimeError('Abstract method _changer called') 391 392class FillPairedData(list): 393 def __init__(self,v,other=0): 394 list.__init__(self,v) 395 self.other = other 396