utils.py 11 KB

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