axes.py 89 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296
  1. #Copyright ReportLab Europe Ltd. 2000-2017
  2. #see license.txt for license details
  3. __version__='3.3.0'
  4. __doc__="""Collection of axes for charts.
  5. The current collection comprises axes for charts using cartesian
  6. coordinate systems. All axes might have tick marks and labels.
  7. There are two dichotomies for axes: one of X and Y flavours and
  8. another of category and value flavours.
  9. Category axes have an ordering but no metric. They are divided
  10. into a number of equal-sized buckets. Their tick marks or labels,
  11. if available, go BETWEEN the buckets, and the labels are placed
  12. below to/left of the X/Y-axis, respectively.
  13. Value axes have an ordering AND metric. They correspond to a nu-
  14. meric quantity. Value axis have a real number quantity associated
  15. with it. The chart tells it where to go.
  16. The most basic axis divides the number line into equal spaces
  17. and has tickmarks and labels associated with each; later we
  18. will add variants where you can specify the sampling
  19. interval.
  20. The charts using axis tell them where the labels should be placed.
  21. Axes of complementary X/Y flavours can be connected to each other
  22. in various ways, i.e. with a specific reference point, like an
  23. x/value axis to a y/value (or category) axis. In this case the
  24. connection can be either at the top or bottom of the former or
  25. at any absolute value (specified in points) or at some value of
  26. the former axes in its own coordinate system.
  27. """
  28. from math import log10 as math_log10
  29. from reportlab.lib.validators import isNumber, isNumberOrNone, isListOfStringsOrNone, isListOfNumbers, \
  30. isListOfNumbersOrNone, isColorOrNone, OneOf, isBoolean, SequenceOf, \
  31. isString, EitherOr, Validator, NoneOr, isInstanceOf, \
  32. isNormalDate, isNoneOrCallable
  33. from reportlab.lib.attrmap import *
  34. from reportlab.lib import normalDate
  35. from reportlab.graphics.shapes import Drawing, Line, PolyLine, Rect, Group, STATE_DEFAULTS, _textBoxLimits, _rotatedBoxLimits
  36. from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection
  37. from reportlab.graphics.charts.textlabels import Label, PMVLabel, XLabel, DirectDrawFlowable
  38. from reportlab.graphics.charts.utils import nextRoundNumber
  39. from reportlab.graphics.widgets.grids import ShadedRect
  40. from reportlab.lib.colors import Color
  41. from reportlab.lib.utils import isSeq
  42. import copy
  43. try:
  44. reduce # Python 2.x
  45. except NameError:
  46. from functools import reduce
  47. # Helpers.
  48. def _findMinMaxValue(V, x, default, func, special=None):
  49. if isSeq(V[0][0]):
  50. if special:
  51. f=lambda T,x=x,special=special,func=func: special(T,x,func)
  52. else:
  53. f=lambda T,x=x: T[x]
  54. V=list(map(lambda e,f=f: list(map(f,e)),V))
  55. V = list(filter(len,[[x for x in x if x is not None] for x in V]))
  56. if len(V)==0: return default
  57. return func(list(map(func,V)))
  58. def _findMin(V, x, default,special=None):
  59. '''find minimum over V[i][x]'''
  60. return _findMinMaxValue(V,x,default,min,special=special)
  61. def _findMax(V, x, default,special=None):
  62. '''find maximum over V[i][x]'''
  63. return _findMinMaxValue(V,x,default,max,special=special)
  64. def _allInt(values):
  65. '''true if all values are int'''
  66. for v in values:
  67. try:
  68. if int(v)!=v: return 0
  69. except:
  70. return 0
  71. return 1
  72. class AxisLabelAnnotation:
  73. '''Create a grid like line using the given user value to draw the line
  74. v value to use
  75. kwds may contain
  76. scaleValue True/not given --> scale the value
  77. otherwise use the absolute value
  78. labelClass the label class to use default Label
  79. all Label keywords are acceptable (including say _text)
  80. '''
  81. def __init__(self,v,**kwds):
  82. self._v = v
  83. self._kwds = kwds
  84. def __call__(self,axis):
  85. kwds = self._kwds.copy()
  86. labelClass = kwds.pop('labelClass',Label)
  87. scaleValue = kwds.pop('scaleValue',True)
  88. if not hasattr(axis,'_tickValues'):
  89. axis._pseudo_configure()
  90. sv = (axis.scale if scaleValue else lambda x: x)(self._v)
  91. if axis.isYAxis:
  92. x = axis._x
  93. y = sv
  94. else:
  95. x = sv
  96. y = axis._y
  97. kwds['x'] = x
  98. kwds['y'] = y
  99. return labelClass(**kwds)
  100. class AxisLineAnnotation:
  101. '''Create a grid like line using the given user value to draw the line
  102. kwds may contain
  103. startOffset if true v is offset from the default grid start position
  104. endOffset if true v is offset from the default grid end position
  105. scaleValue True/not given --> scale the value
  106. otherwise use the absolute value
  107. lo lowest coordinate to draw default 0
  108. hi highest coordinate to draw at default = length
  109. drawAtLimit True draw line at appropriate limit if its coordinate exceeds the lo, hi range
  110. False ignore if it's outside the range
  111. all Line keywords are acceptable
  112. '''
  113. def __init__(self,v,**kwds):
  114. self._v = v
  115. self._kwds = kwds
  116. def __call__(self,axis):
  117. kwds = self._kwds.copy()
  118. scaleValue = kwds.pop('scaleValue',True)
  119. endOffset = kwds.pop('endOffset',False)
  120. startOffset = kwds.pop('startOffset',False)
  121. if axis.isYAxis:
  122. offs = axis._x
  123. d0 = axis._y
  124. else:
  125. offs = axis._y
  126. d0 = axis._x
  127. s = kwds.pop('start',None)
  128. e = kwds.pop('end',None)
  129. if s is None or e is None:
  130. dim = getattr(getattr(axis,'joinAxis',None),'getGridDims',None)
  131. if dim and hasattr(dim,'__call__'):
  132. dim = dim()
  133. if dim:
  134. if s is None: s = dim[0]
  135. if e is None: e = dim[1]
  136. else:
  137. if s is None: s = 0
  138. if e is None: e = 0
  139. hi = kwds.pop('hi',axis._length)+d0
  140. lo = kwds.pop('lo',0)+d0
  141. lo,hi=min(lo,hi),max(lo,hi)
  142. drawAtLimit = kwds.pop('drawAtLimit',False)
  143. oaglp = axis._get_line_pos
  144. if not scaleValue:
  145. axis._get_line_pos = lambda x: x
  146. try:
  147. v = self._v
  148. if endOffset:
  149. v = v + hi
  150. elif startOffset:
  151. v = v + lo
  152. func = axis._getLineFunc(s-offs,e-offs,kwds.pop('parent',None))
  153. if not hasattr(axis,'_tickValues'):
  154. axis._pseudo_configure()
  155. d = axis._get_line_pos(v)
  156. if d<lo or d>hi:
  157. if not drawAtLimit: return None
  158. if d<lo:
  159. d = lo
  160. else:
  161. d = hi
  162. axis._get_line_pos = lambda x: d
  163. L = func(v)
  164. for k,v in kwds.items():
  165. setattr(L,k,v)
  166. finally:
  167. axis._get_line_pos = oaglp
  168. return L
  169. class AxisBackgroundAnnotation:
  170. '''Create a set of coloured bars on the background of a chart using axis ticks as the bar borders
  171. colors is a set of colors to use for the background bars. A colour of None is just a skip.
  172. Special effects if you pass a rect or Shaded rect instead.
  173. '''
  174. def __init__(self,colors,**kwds):
  175. self._colors = colors
  176. self._kwds = kwds
  177. def __call__(self,axis):
  178. colors = self._colors
  179. if not colors: return
  180. kwds = self._kwds.copy()
  181. isYAxis = axis.isYAxis
  182. if isYAxis:
  183. offs = axis._x
  184. d0 = axis._y
  185. else:
  186. offs = axis._y
  187. d0 = axis._x
  188. s = kwds.pop('start',None)
  189. e = kwds.pop('end',None)
  190. if s is None or e is None:
  191. dim = getattr(getattr(axis,'joinAxis',None),'getGridDims',None)
  192. if dim and hasattr(dim,'__call__'):
  193. dim = dim()
  194. if dim:
  195. if s is None: s = dim[0]
  196. if e is None: e = dim[1]
  197. else:
  198. if s is None: s = 0
  199. if e is None: e = 0
  200. if not hasattr(axis,'_tickValues'):
  201. axis._pseudo_configure()
  202. tv = getattr(axis,'_tickValues',None)
  203. if not tv: return
  204. G = Group()
  205. ncolors = len(colors)
  206. v0 = axis._get_line_pos(tv[0])
  207. for i in range(1,len(tv)):
  208. v1 = axis._get_line_pos(tv[i])
  209. c = colors[(i-1)%ncolors]
  210. if c:
  211. if isYAxis:
  212. y = v0
  213. x = s
  214. height = v1-v0
  215. width = e-s
  216. else:
  217. x = v0
  218. y = s
  219. width = v1-v0
  220. height = e-s
  221. if isinstance(c,Color):
  222. r = Rect(x,y,width,height,fillColor=c,strokeColor=None)
  223. elif isinstance(c,Rect):
  224. r = Rect(x,y,width,height)
  225. for k in c.__dict__:
  226. if k not in ('x','y','width','height'):
  227. setattr(r,k,getattr(c,k))
  228. elif isinstance(c,ShadedRect):
  229. r = ShadedRect(x=x,y=y,width=width,height=height)
  230. for k in c.__dict__:
  231. if k not in ('x','y','width','height'):
  232. setattr(r,k,getattr(c,k))
  233. G.add(r)
  234. v0 = v1
  235. return G
  236. class TickLU:
  237. '''lookup special cases for tick values'''
  238. def __init__(self,*T,**kwds):
  239. self.accuracy = kwds.pop('accuracy',1e-8)
  240. self.T = T
  241. def __contains__(self,t):
  242. accuracy = self.accuracy
  243. for x,v in self.T:
  244. if abs(x-t)<accuracy:
  245. return True
  246. return False
  247. def __getitem__(self,t):
  248. accuracy = self.accuracy
  249. for x,v in self.T:
  250. if abs(x-t)<self.accuracy:
  251. return v
  252. raise IndexError('cannot locate index %r' % t)
  253. class _AxisG(Widget):
  254. def _get_line_pos(self,v):
  255. v = self.scale(v)
  256. try:
  257. v = v[0]
  258. except:
  259. pass
  260. return v
  261. def _cxLine(self,x,start,end):
  262. x = self._get_line_pos(x)
  263. return Line(x, self._y + start, x, self._y + end)
  264. def _cyLine(self,y,start,end):
  265. y = self._get_line_pos(y)
  266. return Line(self._x + start, y, self._x + end, y)
  267. def _cxLine3d(self,x,start,end,_3d_dx,_3d_dy):
  268. x = self._get_line_pos(x)
  269. y0 = self._y + start
  270. y1 = self._y + end
  271. y0, y1 = min(y0,y1),max(y0,y1)
  272. x1 = x + _3d_dx
  273. return PolyLine([x,y0,x1,y0+_3d_dy,x1,y1+_3d_dy],strokeLineJoin=1)
  274. def _cyLine3d(self,y,start,end,_3d_dx,_3d_dy):
  275. y = self._get_line_pos(y)
  276. x0 = self._x + start
  277. x1 = self._x + end
  278. x0, x1 = min(x0,x1),max(x0,x1)
  279. y1 = y + _3d_dy
  280. return PolyLine([x0,y,x0+_3d_dx,y1,x1+_3d_dx,y1],strokeLineJoin=1)
  281. def _getLineFunc(self, start, end, parent=None):
  282. _3d_dx = getattr(parent,'_3d_dx',None)
  283. if _3d_dx is not None:
  284. _3d_dy = getattr(parent,'_3d_dy',None)
  285. f = self.isYAxis and self._cyLine3d or self._cxLine3d
  286. return lambda v, s=start, e=end, f=f,_3d_dx=_3d_dx,_3d_dy=_3d_dy: f(v,s,e,_3d_dx=_3d_dx,_3d_dy=_3d_dy)
  287. else:
  288. f = self.isYAxis and self._cyLine or self._cxLine
  289. return lambda v, s=start, e=end, f=f: f(v,s,e)
  290. def _makeLines(self,g,start,end,strokeColor,strokeWidth,strokeDashArray,strokeLineJoin,strokeLineCap,strokeMiterLimit,parent=None,exclude=[],specials={}):
  291. func = self._getLineFunc(start,end,parent)
  292. if not hasattr(self,'_tickValues'):
  293. self._pseudo_configure()
  294. if exclude:
  295. exf = self.isYAxis and (lambda l: l.y1 in exclude) or (lambda l: l.x1 in exclude)
  296. else:
  297. exf = None
  298. for t in self._tickValues:
  299. L = func(t)
  300. if exf and exf(L): continue
  301. L.strokeColor = strokeColor
  302. L.strokeWidth = strokeWidth
  303. L.strokeDashArray = strokeDashArray
  304. L.strokeLineJoin = strokeLineJoin
  305. L.strokeLineCap = strokeLineCap
  306. L.strokeMiterLimit = strokeMiterLimit
  307. if t in specials:
  308. for a,v in specials[t].items():
  309. setattr(L,a,v)
  310. g.add(L)
  311. def makeGrid(self,g,dim=None,parent=None,exclude=[]):
  312. '''this is only called by a container object'''
  313. c = self.gridStrokeColor
  314. w = self.gridStrokeWidth or 0
  315. if w and c and self.visibleGrid:
  316. s = self.gridStart
  317. e = self.gridEnd
  318. if s is None or e is None:
  319. if dim and hasattr(dim,'__call__'):
  320. dim = dim()
  321. if dim:
  322. if s is None: s = dim[0]
  323. if e is None: e = dim[1]
  324. else:
  325. if s is None: s = 0
  326. if e is None: e = 0
  327. if s or e:
  328. if self.isYAxis: offs = self._x
  329. else: offs = self._y
  330. self._makeLines(g,s-offs,e-offs,c,w,self.gridStrokeDashArray,self.gridStrokeLineJoin,self.gridStrokeLineCap,self.gridStrokeMiterLimit,parent=parent,exclude=exclude,specials=getattr(self,'_gridSpecials',{}))
  331. self._makeSubGrid(g,dim,parent,exclude=[])
  332. def _makeSubGrid(self,g,dim=None,parent=None,exclude=[]):
  333. '''this is only called by a container object'''
  334. if not (getattr(self,'visibleSubGrid',0) and self.subTickNum>0): return
  335. c = self.subGridStrokeColor
  336. w = self.subGridStrokeWidth or 0
  337. if not(w and c): return
  338. s = self.subGridStart
  339. e = self.subGridEnd
  340. if s is None or e is None:
  341. if dim and hasattr(dim,'__call__'):
  342. dim = dim()
  343. if dim:
  344. if s is None: s = dim[0]
  345. if e is None: e = dim[1]
  346. else:
  347. if s is None: s = 0
  348. if e is None: e = 0
  349. if s or e:
  350. if self.isYAxis: offs = self._x
  351. else: offs = self._y
  352. otv = self._calcSubTicks()
  353. try:
  354. self._makeLines(g,s-offs,e-offs,c,w,self.subGridStrokeDashArray,self.subGridStrokeLineJoin,self.subGridStrokeLineCap,self.subGridStrokeMiterLimit,parent=parent,exclude=exclude)
  355. finally:
  356. self._tickValues = otv
  357. def getGridDims(self,start=None,end=None):
  358. if start is None: start = (self._x,self._y)[self.isYAxis]
  359. if end is None: end = start+self._length
  360. return start,end
  361. def isYAxis(self):
  362. if getattr(self,'_dataIndex',None)==1: return True
  363. acn = self.__class__.__name__
  364. return acn[0]=='Y' or acn[:4]=='AdjY'
  365. isYAxis = property(isYAxis)
  366. def isXAxis(self):
  367. if getattr(self,'_dataIndex',None)==0: return True
  368. acn = self.__class__.__name__
  369. return acn[0]=='X' or acn[:11]=='NormalDateX'
  370. isXAxis = property(isXAxis)
  371. def addAnnotations(self,g,A=None):
  372. if A is None: getattr(self,'annotations',[])
  373. for x in A:
  374. g.add(x(self))
  375. def _splitAnnotations(self):
  376. A = getattr(self,'annotations',[])[:]
  377. D = {}
  378. for v in ('early','beforeAxis','afterAxis','beforeTicks',
  379. 'afterTicks','beforeTickLabels',
  380. 'afterTickLabels','late'):
  381. R = [].append
  382. P = [].append
  383. for a in A:
  384. if getattr(a,v,0):
  385. R(a)
  386. else:
  387. P(a)
  388. D[v] = R.__self__
  389. A[:] = P.__self__
  390. D['late'] += A
  391. return D
  392. def draw(self):
  393. g = Group()
  394. A = self._splitAnnotations()
  395. self.addAnnotations(g,A['early'])
  396. if self.visible:
  397. self.addAnnotations(g,A['beforeAxis'])
  398. g.add(self.makeAxis())
  399. self.addAnnotations(g,A['afterAxis'])
  400. self.addAnnotations(g,A['beforeTicks'])
  401. g.add(self.makeTicks())
  402. self.addAnnotations(g,A['afterTicks'])
  403. self.addAnnotations(g,A['beforeTickLabels'])
  404. g.add(self.makeTickLabels())
  405. self.addAnnotations(g,A['afterTickLabels'])
  406. self.addAnnotations(g,A['late'])
  407. return g
  408. class CALabel(PMVLabel):
  409. _attrMap = AttrMap(BASE=PMVLabel,
  410. labelPosFrac = AttrMapValue(isNumber, desc='where in the category range [0,1] the labels should be anchored'),
  411. )
  412. def __init__(self,**kw):
  413. PMVLabel.__init__(self,**kw)
  414. self._setKeywords(
  415. labelPosFrac = 0.5,
  416. )
  417. # Category axes.
  418. class CategoryAxis(_AxisG):
  419. "Abstract category axis, unusable in itself."
  420. _nodoc = 1
  421. _attrMap = AttrMap(
  422. visible = AttrMapValue(isBoolean, desc='Display entire object, if true.'),
  423. visibleAxis = AttrMapValue(isBoolean, desc='Display axis line, if true.'),
  424. visibleTicks = AttrMapValue(isBoolean, desc='Display axis ticks, if true.'),
  425. visibleLabels = AttrMapValue(isBoolean, desc='Display axis labels, if true.'),
  426. visibleGrid = AttrMapValue(isBoolean, desc='Display axis grid, if true.'),
  427. strokeWidth = AttrMapValue(isNumber, desc='Width of axis line and ticks.'),
  428. strokeColor = AttrMapValue(isColorOrNone, desc='Color of axis line and ticks.'),
  429. strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for axis line.'),
  430. strokeLineCap = AttrMapValue(OneOf(0,1,2),desc="Line cap 0=butt, 1=round & 2=square"),
  431. strokeLineJoin = AttrMapValue(OneOf(0,1,2),desc="Line join 0=miter, 1=round & 2=bevel"),
  432. strokeMiterLimit = AttrMapValue(isNumber,desc="miter limit control miter line joins"),
  433. gridStrokeWidth = AttrMapValue(isNumber, desc='Width of grid lines.'),
  434. gridStrokeColor = AttrMapValue(isColorOrNone, desc='Color of grid lines.'),
  435. gridStrokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for grid lines.'),
  436. gridStrokeLineCap = AttrMapValue(OneOf(0,1,2),desc="Grid Line cap 0=butt, 1=round & 2=square"),
  437. gridStrokeLineJoin = AttrMapValue(OneOf(0,1,2),desc="Grid Line join 0=miter, 1=round & 2=bevel"),
  438. gridStrokeMiterLimit = AttrMapValue(isNumber,desc="Grid miter limit control miter line joins"),
  439. gridStart = AttrMapValue(isNumberOrNone, desc='Start of grid lines wrt axis origin'),
  440. gridEnd = AttrMapValue(isNumberOrNone, desc='End of grid lines wrt axis origin'),
  441. drawGridLast = AttrMapValue(isBoolean, desc='if true draw gridlines after everything else.'),
  442. labels = AttrMapValue(None, desc='Handle of the axis labels.'),
  443. categoryNames = AttrMapValue(isListOfStringsOrNone, desc='List of category names.'),
  444. joinAxis = AttrMapValue(None, desc='Join both axes if true.'),
  445. joinAxisPos = AttrMapValue(isNumberOrNone, desc='Position at which to join with other axis.'),
  446. reverseDirection = AttrMapValue(isBoolean, desc='If true reverse category direction.'),
  447. style = AttrMapValue(OneOf('parallel','stacked','parallel_3d'),"How common category bars are plotted"),
  448. labelAxisMode = AttrMapValue(OneOf('high','low','axis', 'axispmv'), desc="Like joinAxisMode, but for the axis labels"),
  449. tickShift = AttrMapValue(isBoolean, desc='Tick shift typically'),
  450. tickStrokeWidth = AttrMapValue(isNumberOrNone, desc='Width of ticks if specified.'),
  451. tickStrokeColor = AttrMapValue(isColorOrNone, desc='Color of ticks if specified.'),
  452. loPad = AttrMapValue(isNumber, desc='extra inner space before start of the axis'),
  453. hiPad = AttrMapValue(isNumber, desc='extra inner space after end of the axis'),
  454. annotations = AttrMapValue(None,desc='list of annotations'),
  455. loLLen = AttrMapValue(isNumber, desc='extra line length before start of the axis'),
  456. hiLLen = AttrMapValue(isNumber, desc='extra line length after end of the axis'),
  457. skipGrid = AttrMapValue(OneOf('none','top','both','bottom'),"grid lines to skip top bottom both none"),
  458. innerTickDraw = AttrMapValue(isNoneOrCallable, desc="Callable to replace _drawInnerTicks"),
  459. )
  460. def __init__(self):
  461. assert self.__class__.__name__!='CategoryAxis', "Abstract Class CategoryAxis Instantiated"
  462. # private properties set by methods. The initial values
  463. # here are to make demos easy; they would always be
  464. # overridden in real life.
  465. self._x = 50
  466. self._y = 50
  467. self._length = 100
  468. self._catCount = 0
  469. # public properties
  470. self.visible = 1
  471. self.visibleAxis = 1
  472. self.visibleTicks = 1
  473. self.visibleLabels = 1
  474. self.visibleGrid = 0
  475. self.drawGridLast = False
  476. self.strokeWidth = 1
  477. self.strokeColor = STATE_DEFAULTS['strokeColor']
  478. self.strokeDashArray = STATE_DEFAULTS['strokeDashArray']
  479. self.gridStrokeLineJoin = self.strokeLineJoin = STATE_DEFAULTS['strokeLineJoin']
  480. self.gridStrokeLineCap = self.strokeLineCap = STATE_DEFAULTS['strokeLineCap']
  481. self.gridStrokeMiterLimit = self.strokeMiterLimit = STATE_DEFAULTS['strokeMiterLimit']
  482. self.gridStrokeWidth = 0.25
  483. self.gridStrokeColor = STATE_DEFAULTS['strokeColor']
  484. self.gridStrokeDashArray = STATE_DEFAULTS['strokeDashArray']
  485. self.gridStart = self.gridEnd = None
  486. self.strokeLineJoin = STATE_DEFAULTS['strokeLineJoin']
  487. self.strokeLineCap = STATE_DEFAULTS['strokeLineCap']
  488. self.strokeMiterLimit = STATE_DEFAULTS['strokeMiterLimit']
  489. self.labels = TypedPropertyCollection(CALabel)
  490. # if None, they don't get labels. If provided,
  491. # you need one name per data point and they are
  492. # used for label text.
  493. self.categoryNames = None
  494. self.joinAxis = None
  495. self.joinAxisPos = None
  496. self.joinAxisMode = None
  497. self.labelAxisMode = 'axis'
  498. self.reverseDirection = 0
  499. self.style = 'parallel'
  500. #various private things which need to be initialized
  501. self._labelTextFormat = None
  502. self.tickShift = 0
  503. self.loPad = 0
  504. self.hiPad = 0
  505. self.loLLen = 0
  506. self.hiLLen = 0
  507. def setPosition(self, x, y, length):
  508. # ensure floating point
  509. self._x = float(x)
  510. self._y = float(y)
  511. self._length = float(length)
  512. def configure(self, multiSeries,barWidth=None):
  513. self._catCount = max(list(map(len,multiSeries)))
  514. self._barWidth = barWidth or ((self._length-self.loPad-self.hiPad)/float(self._catCount or 1))
  515. self._calcTickmarkPositions()
  516. if self.labelAxisMode == 'axispmv':
  517. self._pmv = [sum([series[i] for series in multiSeries]) for i in range(self._catCount)]
  518. def _calcTickmarkPositions(self):
  519. n = self._catCount
  520. if self.tickShift:
  521. self._tickValues = [t+0.5 for t in range(n)]
  522. else:
  523. if self.reverseDirection:
  524. self._tickValues = list(range(-1,n))
  525. else:
  526. self._tickValues = list(range(n+1))
  527. def _scale(self,idx):
  528. if self.reverseDirection: idx = self._catCount-idx-1
  529. return idx
  530. def scale(self, idx):
  531. "Returns the position and width in drawing units"
  532. return (self.loScale(idx), self._barWidth)
  533. def midScale(self, idx):
  534. "Returns the bar mid position in drawing units"
  535. return self.loScale(idx) + 0.5*self._barWidth
  536. def _assertYAxis(axis):
  537. assert axis.isYAxis, "Cannot connect to other axes (%s), but Y- ones." % axis.__class__.__name__
  538. def _assertXAxis(axis):
  539. assert axis.isXAxis, "Cannot connect to other axes (%s), but X- ones." % axis.__class__.__name__
  540. class _XTicks:
  541. _tickTweaks = 0 #try 0.25-0.5
  542. @property
  543. def actualTickStrokeWidth(self):
  544. return getattr(self,'tickStrokeWidth',self.strokeWidth)
  545. @property
  546. def actualTickStrokeColor(self):
  547. return getattr(self,'tickStrokeColor',self.strokeColor)
  548. def _drawTicksInner(self,tU,tD,g):
  549. itd = getattr(self,'innerTickDraw',None)
  550. if itd:
  551. itd(self,tU,tD,g)
  552. elif tU or tD:
  553. sW = self.actualTickStrokeWidth
  554. tW = self._tickTweaks
  555. if tW:
  556. if tU and not tD:
  557. tD = tW*sW
  558. elif tD and not tU:
  559. tU = tW*sW
  560. self._makeLines(g,tU,-tD,self.actualTickStrokeColor,sW,self.strokeDashArray,self.strokeLineJoin,self.strokeLineCap,self.strokeMiterLimit)
  561. def _drawTicks(self,tU,tD,g=None):
  562. g = g or Group()
  563. if self.visibleTicks:
  564. self._drawTicksInner(tU,tD,g)
  565. return g
  566. def _drawSubTicks(self,tU,tD,g):
  567. if getattr(self,'visibleSubTicks',0) and self.subTickNum>0:
  568. otv = self._calcSubTicks()
  569. try:
  570. self._subTicking = 1
  571. self._drawTicksInner(tU,tD,g)
  572. finally:
  573. del self._subTicking
  574. self._tickValues = otv
  575. def makeTicks(self):
  576. yold=self._y
  577. try:
  578. self._y = self._labelAxisPos(getattr(self,'tickAxisMode','axis'))
  579. g = self._drawTicks(self.tickUp,self.tickDown)
  580. self._drawSubTicks(getattr(self,'subTickHi',0),getattr(self,'subTickLo',0),g)
  581. return g
  582. finally:
  583. self._y = yold
  584. def _labelAxisPos(self,mode=None):
  585. axis = self.joinAxis
  586. if axis:
  587. mode = mode or self.labelAxisMode
  588. if mode == 'low':
  589. return axis._y
  590. elif mode == 'high':
  591. return axis._y + axis._length
  592. return self._y
  593. class _YTicks(_XTicks):
  594. def _labelAxisPos(self,mode=None):
  595. axis = self.joinAxis
  596. if axis:
  597. mode = mode or self.labelAxisMode
  598. if mode == 'low':
  599. return axis._x
  600. elif mode == 'high':
  601. return axis._x + axis._length
  602. return self._x
  603. def makeTicks(self):
  604. xold=self._x
  605. try:
  606. self._x = self._labelAxisPos(getattr(self,'tickAxisMode','axis'))
  607. g = self._drawTicks(self.tickRight,self.tickLeft)
  608. self._drawSubTicks(getattr(self,'subTickHi',0),getattr(self,'subTickLo',0),g)
  609. return g
  610. finally:
  611. self._x = xold
  612. class XCategoryAxis(_XTicks,CategoryAxis):
  613. "X/category axis"
  614. _attrMap = AttrMap(BASE=CategoryAxis,
  615. tickUp = AttrMapValue(isNumber,
  616. desc='Tick length up the axis.'),
  617. tickDown = AttrMapValue(isNumber,
  618. desc='Tick length down the axis.'),
  619. joinAxisMode = AttrMapValue(OneOf('bottom', 'top', 'value', 'points', None),
  620. desc="Mode used for connecting axis ('bottom', 'top', 'value', 'points', None)."),
  621. )
  622. _dataIndex = 0
  623. def __init__(self):
  624. CategoryAxis.__init__(self)
  625. self.labels.boxAnchor = 'n' #north - top edge
  626. self.labels.dy = -5
  627. # ultra-simple tick marks for now go between categories
  628. # and have same line style as axis - need more
  629. self.tickUp = 0 # how far into chart does tick go?
  630. self.tickDown = 5 # how far below axis does tick go?
  631. def demo(self):
  632. self.setPosition(30, 70, 140)
  633. self.configure([(10,20,30,40,50)])
  634. self.categoryNames = ['One','Two','Three','Four','Five']
  635. # all labels top-centre aligned apart from the last
  636. self.labels.boxAnchor = 'n'
  637. self.labels[4].boxAnchor = 'e'
  638. self.labels[4].angle = 90
  639. d = Drawing(200, 100)
  640. d.add(self)
  641. return d
  642. def joinToAxis(self, yAxis, mode='bottom', pos=None):
  643. "Join with y-axis using some mode."
  644. _assertYAxis(yAxis)
  645. if mode == 'bottom':
  646. self._y = yAxis._y
  647. elif mode == 'top':
  648. self._y = yAxis._y + yAxis._length
  649. elif mode == 'value':
  650. self._y = yAxis.scale(pos)
  651. elif mode == 'points':
  652. self._y = pos
  653. def _joinToAxis(self):
  654. ja = self.joinAxis
  655. if ja:
  656. jam = self.joinAxisMode
  657. if jam in ('bottom', 'top'):
  658. self.joinToAxis(ja, mode=jam)
  659. elif jam in ('value', 'points'):
  660. self.joinToAxis(ja, mode=jam, pos=self.joinAxisPos)
  661. def loScale(self, idx):
  662. """returns the x position in drawing units"""
  663. return self._x + self.loPad + self._scale(idx)*self._barWidth
  664. def makeAxis(self):
  665. g = Group()
  666. self._joinToAxis()
  667. if not self.visibleAxis: return g
  668. axis = Line(self._x-self.loLLen, self._y, self._x + self._length+self.hiLLen, self._y)
  669. axis.strokeColor = self.strokeColor
  670. axis.strokeWidth = self.strokeWidth
  671. axis.strokeDashArray = self.strokeDashArray
  672. g.add(axis)
  673. return g
  674. def makeTickLabels(self):
  675. g = Group()
  676. if not self.visibleLabels: return g
  677. categoryNames = self.categoryNames
  678. if categoryNames is not None:
  679. catCount = self._catCount
  680. n = len(categoryNames)
  681. reverseDirection = self.reverseDirection
  682. barWidth = self._barWidth
  683. _y = self._labelAxisPos()
  684. _x = self._x
  685. pmv = self._pmv if self.labelAxisMode=='axispmv' else None
  686. for i in range(catCount):
  687. if reverseDirection: ic = catCount-i-1
  688. else: ic = i
  689. if ic>=n: continue
  690. label=i-catCount
  691. if label in self.labels:
  692. label = self.labels[label]
  693. else:
  694. label = self.labels[i]
  695. if pmv:
  696. _dy = label.dy
  697. v = label._pmv = pmv[ic]
  698. if v<0: _dy *= -2
  699. else:
  700. _dy = 0
  701. lpf = label.labelPosFrac
  702. x = _x + (i+lpf) * barWidth
  703. label.setOrigin(x,_y+_dy)
  704. label.setText(categoryNames[ic] or '')
  705. g.add(label)
  706. return g
  707. class YCategoryAxis(_YTicks,CategoryAxis):
  708. "Y/category axis"
  709. _attrMap = AttrMap(BASE=CategoryAxis,
  710. tickLeft = AttrMapValue(isNumber,
  711. desc='Tick length left of the axis.'),
  712. tickRight = AttrMapValue(isNumber,
  713. desc='Tick length right of the axis.'),
  714. joinAxisMode = AttrMapValue(OneOf(('left', 'right', 'value', 'points', None)),
  715. desc="Mode used for connecting axis ('left', 'right', 'value', 'points', None)."),
  716. )
  717. _dataIndex = 1
  718. def __init__(self):
  719. CategoryAxis.__init__(self)
  720. self.labels.boxAnchor = 'e' #east - right edge
  721. self.labels.dx = -5
  722. # ultra-simple tick marks for now go between categories
  723. # and have same line style as axis - need more
  724. self.tickLeft = 5 # how far left of axis does tick go?
  725. self.tickRight = 0 # how far right of axis does tick go?
  726. def demo(self):
  727. self.setPosition(50, 10, 80)
  728. self.configure([(10,20,30)])
  729. self.categoryNames = ['One','Two','Three']
  730. # all labels top-centre aligned apart from the last
  731. self.labels.boxAnchor = 'e'
  732. self.labels[2].boxAnchor = 's'
  733. self.labels[2].angle = 90
  734. d = Drawing(200, 100)
  735. d.add(self)
  736. return d
  737. def joinToAxis(self, xAxis, mode='left', pos=None):
  738. "Join with x-axis using some mode."
  739. _assertXAxis(xAxis)
  740. if mode == 'left':
  741. self._x = xAxis._x * 1.0
  742. elif mode == 'right':
  743. self._x = (xAxis._x + xAxis._length) * 1.0
  744. elif mode == 'value':
  745. self._x = xAxis.scale(pos) * 1.0
  746. elif mode == 'points':
  747. self._x = pos * 1.0
  748. def _joinToAxis(self):
  749. ja = self.joinAxis
  750. if ja:
  751. jam = self.joinAxisMode
  752. if jam in ('left', 'right'):
  753. self.joinToAxis(ja, mode=jam)
  754. elif jam in ('value', 'points'):
  755. self.joinToAxis(ja, mode=jam, pos=self.joinAxisPos)
  756. def loScale(self, idx):
  757. "Returns the y position in drawing units"
  758. return self._y + self._scale(idx)*self._barWidth
  759. def makeAxis(self):
  760. g = Group()
  761. self._joinToAxis()
  762. if not self.visibleAxis: return g
  763. axis = Line(self._x, self._y-self.loLLen, self._x, self._y + self._length+self.hiLLen)
  764. axis.strokeColor = self.strokeColor
  765. axis.strokeWidth = self.strokeWidth
  766. axis.strokeDashArray = self.strokeDashArray
  767. g.add(axis)
  768. return g
  769. def makeTickLabels(self):
  770. g = Group()
  771. if not self.visibleLabels: return g
  772. categoryNames = self.categoryNames
  773. if categoryNames is not None:
  774. catCount = self._catCount
  775. n = len(categoryNames)
  776. reverseDirection = self.reverseDirection
  777. barWidth = self._barWidth
  778. labels = self.labels
  779. _x = self._labelAxisPos()
  780. _y = self._y
  781. pmv = self._pmv if self.labelAxisMode=='axispmv' else None
  782. for i in range(catCount):
  783. if reverseDirection: ic = catCount-i-1
  784. else: ic = i
  785. if ic>=n: continue
  786. label=i-catCount
  787. if label in self.labels:
  788. label = self.labels[label]
  789. else:
  790. label = self.labels[i]
  791. lpf = label.labelPosFrac
  792. y = _y + (i+lpf) * barWidth
  793. if pmv:
  794. _dx = label.dx
  795. v = label._pmv = pmv[ic]
  796. if v<0: _dx *= -2
  797. else:
  798. _dx = 0
  799. label.setOrigin(_x+_dx, y)
  800. label.setText(categoryNames[ic] or '')
  801. g.add(label)
  802. return g
  803. class TickLabeller:
  804. '''Abstract base class which may be used to indicate a change
  805. in the call signature for callable label formats
  806. '''
  807. def __call__(self,axis,value):
  808. return 'Abstract class instance called'
  809. #this matches the old python str behaviour
  810. _defaultLabelFormatter = lambda x: '%.12g' % x
  811. # Value axes.
  812. class ValueAxis(_AxisG):
  813. "Abstract value axis, unusable in itself."
  814. _attrMap = AttrMap(
  815. forceZero = AttrMapValue(EitherOr((isBoolean,OneOf('near'))), desc='Ensure zero in range if true.'),
  816. visible = AttrMapValue(isBoolean, desc='Display entire object, if true.'),
  817. visibleAxis = AttrMapValue(isBoolean, desc='Display axis line, if true.'),
  818. visibleLabels = AttrMapValue(isBoolean, desc='Display axis labels, if true.'),
  819. visibleTicks = AttrMapValue(isBoolean, desc='Display axis ticks, if true.'),
  820. visibleGrid = AttrMapValue(isBoolean, desc='Display axis grid, if true.'),
  821. strokeWidth = AttrMapValue(isNumber, desc='Width of axis line and ticks.'),
  822. strokeColor = AttrMapValue(isColorOrNone, desc='Color of axis line and ticks.'),
  823. strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for axis line.'),
  824. strokeLineCap = AttrMapValue(OneOf(0,1,2),desc="Line cap 0=butt, 1=round & 2=square"),
  825. strokeLineJoin = AttrMapValue(OneOf(0,1,2),desc="Line join 0=miter, 1=round & 2=bevel"),
  826. strokeMiterLimit = AttrMapValue(isNumber,desc="miter limit control miter line joins"),
  827. gridStrokeWidth = AttrMapValue(isNumber, desc='Width of grid lines.'),
  828. gridStrokeColor = AttrMapValue(isColorOrNone, desc='Color of grid lines.'),
  829. gridStrokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for grid lines.'),
  830. gridStrokeLineCap = AttrMapValue(OneOf(0,1,2),desc="Grid Line cap 0=butt, 1=round & 2=square"),
  831. gridStrokeLineJoin = AttrMapValue(OneOf(0,1,2),desc="Grid Line join 0=miter, 1=round & 2=bevel"),
  832. gridStrokeMiterLimit = AttrMapValue(isNumber,desc="Grid miter limit control miter line joins"),
  833. gridStart = AttrMapValue(isNumberOrNone, desc='Start of grid lines wrt axis origin'),
  834. gridEnd = AttrMapValue(isNumberOrNone, desc='End of grid lines wrt axis origin'),
  835. drawGridLast = AttrMapValue(isBoolean, desc='if true draw gridlines after everything else.'),
  836. minimumTickSpacing = AttrMapValue(isNumber, desc='Minimum value for distance between ticks.'),
  837. maximumTicks = AttrMapValue(isNumber, desc='Maximum number of ticks.'),
  838. labels = AttrMapValue(None, desc='Handle of the axis labels.'),
  839. labelAxisMode = AttrMapValue(OneOf('high','low','axis'), desc="Like joinAxisMode, but for the axis labels"),
  840. labelTextFormat = AttrMapValue(None, desc='Formatting string or function used for axis labels.'),
  841. labelTextPostFormat = AttrMapValue(None, desc='Extra Formatting string.'),
  842. labelTextScale = AttrMapValue(isNumberOrNone, desc='Scaling for label tick values.'),
  843. valueMin = AttrMapValue(isNumberOrNone, desc='Minimum value on axis.'),
  844. valueMax = AttrMapValue(isNumberOrNone, desc='Maximum value on axis.'),
  845. valueStep = AttrMapValue(isNumberOrNone, desc='Step size used between ticks.'),
  846. valueSteps = AttrMapValue(isListOfNumbersOrNone, desc='List of step sizes used between ticks.'),
  847. avoidBoundFrac = AttrMapValue(EitherOr((isNumberOrNone,SequenceOf(isNumber,emptyOK=0,lo=2,hi=2))), desc='Fraction of interval to allow above and below.'),
  848. avoidBoundSpace = AttrMapValue(EitherOr((isNumberOrNone,SequenceOf(isNumber,emptyOK=0,lo=2,hi=2))), desc='Space to allow above and below.'),
  849. abf_ignore_zero = AttrMapValue(EitherOr((NoneOr(isBoolean),SequenceOf(isBoolean,emptyOK=0,lo=2,hi=2))), desc='Set to True to make the avoidBoundFrac calculations treat zero as non-special'),
  850. rangeRound=AttrMapValue(OneOf('none','both','ceiling','floor'),'How to round the axis limits'),
  851. zrangePref = AttrMapValue(isNumberOrNone, desc='Zero range axis limit preference.'),
  852. style = AttrMapValue(OneOf('normal','stacked','parallel_3d'),"How values are plotted!"),
  853. skipEndL = AttrMapValue(OneOf('none','start','end','both'), desc='Skip high/low tick labels'),
  854. origShiftIPC = AttrMapValue(isNumberOrNone, desc='Lowest label shift interval ratio.'),
  855. origShiftMin = AttrMapValue(isNumberOrNone, desc='Minimum amount to shift.'),
  856. origShiftSpecialValue = AttrMapValue(isNumberOrNone, desc='special value for shift'),
  857. tickAxisMode = AttrMapValue(OneOf('high','low','axis'), desc="Like joinAxisMode, but for the ticks"),
  858. reverseDirection = AttrMapValue(isBoolean, desc='If true reverse category direction.'),
  859. annotations = AttrMapValue(None,desc='list of annotations'),
  860. loLLen = AttrMapValue(isNumber, desc='extra line length before start of the axis'),
  861. hiLLen = AttrMapValue(isNumber, desc='extra line length after end of the axis'),
  862. subTickNum = AttrMapValue(isNumber, desc='Number of axis sub ticks, if >0'),
  863. subTickLo = AttrMapValue(isNumber, desc='sub tick down or left'),
  864. subTickHi = AttrMapValue(isNumber, desc='sub tick up or right'),
  865. visibleSubTicks = AttrMapValue(isBoolean, desc='Display axis sub ticks, if true.'),
  866. visibleSubGrid = AttrMapValue(isBoolean, desc='Display axis sub grid, if true.'),
  867. subGridStrokeWidth = AttrMapValue(isNumber, desc='Width of grid lines.'),
  868. subGridStrokeColor = AttrMapValue(isColorOrNone, desc='Color of grid lines.'),
  869. subGridStrokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for grid lines.'),
  870. subGridStrokeLineCap = AttrMapValue(OneOf(0,1,2),desc="Grid Line cap 0=butt, 1=round & 2=square"),
  871. subGridStrokeLineJoin = AttrMapValue(OneOf(0,1,2),desc="Grid Line join 0=miter, 1=round & 2=bevel"),
  872. subGridStrokeMiterLimit = AttrMapValue(isNumber,desc="Grid miter limit control miter line joins"),
  873. subGridStart = AttrMapValue(isNumberOrNone, desc='Start of grid lines wrt axis origin'),
  874. subGridEnd = AttrMapValue(isNumberOrNone, desc='End of grid lines wrt axis origin'),
  875. tickStrokeWidth = AttrMapValue(isNumber, desc='Width of ticks if specified.'),
  876. subTickStrokeWidth = AttrMapValue(isNumber, desc='Width of sub ticks if specified.'),
  877. subTickStrokeColor = AttrMapValue(isColorOrNone, desc='Color of sub ticks if specified.'),
  878. tickStrokeColor = AttrMapValue(isColorOrNone, desc='Color of ticks if specified.'),
  879. keepTickLabelsInside = AttrMapValue(isBoolean, desc='Ensure tick labels do not project beyond bounds of axis if true'),
  880. skipGrid = AttrMapValue(OneOf('none','top','both','bottom'),"grid lines to skip top bottom both none"),
  881. requiredRange = AttrMapValue(isNumberOrNone, desc='Minimum required value range.'),
  882. innerTickDraw = AttrMapValue(isNoneOrCallable, desc="Callable to replace _drawInnerTicks"),
  883. )
  884. def __init__(self,**kw):
  885. assert self.__class__.__name__!='ValueAxis', 'Abstract Class ValueAxis Instantiated'
  886. self._setKeywords(**kw)
  887. self._setKeywords(
  888. _configured = 0,
  889. # private properties set by methods. The initial values
  890. # here are to make demos easy; they would always be
  891. # overridden in real life.
  892. _x = 50,
  893. _y = 50,
  894. _length = 100,
  895. # public properties
  896. visible = 1,
  897. visibleAxis = 1,
  898. visibleLabels = 1,
  899. visibleTicks = 1,
  900. visibleGrid = 0,
  901. forceZero = 0,
  902. strokeWidth = 1,
  903. strokeColor = STATE_DEFAULTS['strokeColor'],
  904. strokeDashArray = STATE_DEFAULTS['strokeDashArray'],
  905. strokeLineJoin = STATE_DEFAULTS['strokeLineJoin'],
  906. strokeLineCap = STATE_DEFAULTS['strokeLineCap'],
  907. strokeMiterLimit = STATE_DEFAULTS['strokeMiterLimit'],
  908. gridStrokeWidth = 0.25,
  909. gridStrokeColor = STATE_DEFAULTS['strokeColor'],
  910. gridStrokeDashArray = STATE_DEFAULTS['strokeDashArray'],
  911. gridStrokeLineJoin = STATE_DEFAULTS['strokeLineJoin'],
  912. gridStrokeLineCap = STATE_DEFAULTS['strokeLineCap'],
  913. gridStrokeMiterLimit = STATE_DEFAULTS['strokeMiterLimit'],
  914. gridStart = None,
  915. gridEnd = None,
  916. drawGridLast = False,
  917. visibleSubGrid = 0,
  918. visibleSubTicks = 0,
  919. subTickNum = 0,
  920. subTickLo = 0,
  921. subTickHi = 0,
  922. subGridStrokeLineJoin = STATE_DEFAULTS['strokeLineJoin'],
  923. subGridStrokeLineCap = STATE_DEFAULTS['strokeLineCap'],
  924. subGridStrokeMiterLimit = STATE_DEFAULTS['strokeMiterLimit'],
  925. subGridStrokeWidth = 0.25,
  926. subGridStrokeColor = STATE_DEFAULTS['strokeColor'],
  927. subGridStrokeDashArray = STATE_DEFAULTS['strokeDashArray'],
  928. subGridStart = None,
  929. subGridEnd = None,
  930. labels = TypedPropertyCollection(Label),
  931. keepTickLabelsInside = 0,
  932. # how close can the ticks be?
  933. minimumTickSpacing = 10,
  934. maximumTicks = 7,
  935. # a format string like '%0.2f'
  936. # or a function which takes the value as an argument and returns a string
  937. _labelTextFormat = None,
  938. labelAxisMode = 'axis',
  939. labelTextFormat = None,
  940. labelTextPostFormat = None,
  941. labelTextScale = None,
  942. # if set to None, these will be worked out for you.
  943. # if you override any or all of them, your values
  944. # will be used.
  945. valueMin = None,
  946. valueMax = None,
  947. valueStep = None,
  948. avoidBoundFrac = None,
  949. avoidBoundSpace = None,
  950. abf_ignore_zero = False,
  951. rangeRound = 'none',
  952. zrangePref = 0,
  953. style = 'normal',
  954. skipEndL='none',
  955. origShiftIPC = None,
  956. origShiftMin = None,
  957. origShiftSpecialValue = None,
  958. tickAxisMode = 'axis',
  959. reverseDirection=0,
  960. loLLen=0,
  961. hiLLen=0,
  962. requiredRange=0,
  963. )
  964. self.labels.angle = 0
  965. def setPosition(self, x, y, length):
  966. # ensure floating point
  967. self._x = float(x)
  968. self._y = float(y)
  969. self._length = float(length)
  970. def configure(self, dataSeries):
  971. """Let the axis configure its scale and range based on the data.
  972. Called after setPosition. Let it look at a list of lists of
  973. numbers determine the tick mark intervals. If valueMin,
  974. valueMax and valueStep are configured then it
  975. will use them; if any of them are set to None it
  976. will look at the data and make some sensible decision.
  977. You may override this to build custom axes with
  978. irregular intervals. It creates an internal
  979. variable self._values, which is a list of numbers
  980. to use in plotting.
  981. """
  982. self._setRange(dataSeries)
  983. self._configure_end()
  984. def _configure_end(self):
  985. self._calcTickmarkPositions()
  986. self._calcScaleFactor()
  987. self._configured = 1
  988. def _getValueStepAndTicks(self, valueMin, valueMax,cache={}):
  989. try:
  990. K = (valueMin,valueMax)
  991. r = cache[K]
  992. except:
  993. self._valueMin = valueMin
  994. self._valueMax = valueMax
  995. valueStep,T = self._calcStepAndTickPositions()
  996. r = cache[K] = valueStep, T, valueStep*1e-8
  997. return r
  998. def _preRangeAdjust(self,valueMin,valueMax):
  999. rr = self.requiredRange
  1000. if rr>0:
  1001. r = valueMax - valueMin
  1002. if r<rr:
  1003. m = 0.5*(valueMax+valueMin)
  1004. rr *= 0.5
  1005. y1 = min(m-rr,valueMin)
  1006. y2 = max(m+rr,valueMax)
  1007. if valueMin>=100 and y1<100:
  1008. y2 = y2 + 100 - y1
  1009. y1 = 100
  1010. elif valueMin>=0 and y1<0:
  1011. y2 = y2 - y1
  1012. y1 = 0
  1013. valueMin = self._cValueMin = y1
  1014. valueMax = self._cValueMax = y2
  1015. return valueMin,valueMax
  1016. def _setRange(self, dataSeries):
  1017. """Set minimum and maximum axis values.
  1018. The dataSeries argument is assumed to be a list of data
  1019. vectors. Each vector is itself a list or tuple of numbers.
  1020. Returns a min, max tuple.
  1021. """
  1022. oMin = valueMin = self.valueMin
  1023. oMax = valueMax = self.valueMax
  1024. if valueMin is None: valueMin = self._cValueMin = _findMin(dataSeries,self._dataIndex,0)
  1025. if valueMax is None: valueMax = self._cValueMax = _findMax(dataSeries,self._dataIndex,0)
  1026. if valueMin == valueMax:
  1027. if valueMax==0:
  1028. if oMin is None and oMax is None:
  1029. zrp = getattr(self,'zrangePref',0)
  1030. if zrp>0:
  1031. valueMax = zrp
  1032. valueMin = 0
  1033. elif zrp<0:
  1034. valueMax = 0
  1035. valueMin = zrp
  1036. else:
  1037. valueMax = 0.01
  1038. valueMin = -0.01
  1039. elif self.valueMin is None:
  1040. valueMin = -0.01
  1041. else:
  1042. valueMax = 0.01
  1043. else:
  1044. if valueMax>0:
  1045. valueMax = 1.2*valueMax
  1046. valueMin = 0.0
  1047. else:
  1048. valueMax = 0.0
  1049. valueMin = 1.2*valueMin
  1050. if getattr(self,'_bubblePlot',None):
  1051. bubbleMax = float(_findMax(dataSeries,2,0))
  1052. frac=.25
  1053. bubbleV=frac*(valueMax-valueMin)
  1054. self._bubbleV = bubbleV
  1055. self._bubbleMax = bubbleMax
  1056. self._bubbleRadius = frac*self._length
  1057. def special(T,x,func,bubbleV=bubbleV,bubbleMax=bubbleMax):
  1058. try:
  1059. v = T[2]
  1060. except IndexError:
  1061. v = bubbleMAx*0.1
  1062. bubbleV *= (v/bubbleMax)**0.5
  1063. return func(T[x]+bubbleV,T[x]-bubbleV)
  1064. if oMin is None: valueMin = self._cValueMin = _findMin(dataSeries,self._dataIndex,0,special=special)
  1065. if oMax is None: valueMax = self._cValueMax = _findMax(dataSeries,self._dataIndex,0,special=special)
  1066. valueMin, valueMax = self._preRangeAdjust(valueMin,valueMax)
  1067. rangeRound = self.rangeRound
  1068. cMin = valueMin
  1069. cMax = valueMax
  1070. forceZero = self.forceZero
  1071. if forceZero:
  1072. if forceZero=='near':
  1073. forceZero = min(abs(valueMin),abs(valueMax)) <= 5*(valueMax-valueMin)
  1074. if forceZero:
  1075. if valueMax<0: valueMax=0
  1076. elif valueMin>0: valueMin = 0
  1077. abf = self.avoidBoundFrac
  1078. do_rr = not getattr(self,'valueSteps',None)
  1079. do_abf = abf and do_rr
  1080. if not isSeq(abf):
  1081. abf = abf, abf
  1082. abfiz = getattr(self,'abf_ignore_zero', False)
  1083. if not isSeq(abfiz):
  1084. abfiz = abfiz, abfiz
  1085. do_rr = rangeRound != 'none' and do_rr
  1086. if do_rr:
  1087. rrn = rangeRound in ['both','floor']
  1088. rrx = rangeRound in ['both','ceiling']
  1089. else:
  1090. rrn = rrx = 0
  1091. abS = self.avoidBoundSpace
  1092. do_abs = abS
  1093. if do_abs:
  1094. if not isSeq(abS):
  1095. abS = abS, abS
  1096. aL = float(self._length)
  1097. go = do_rr or do_abf or do_abs
  1098. cache = {}
  1099. iter = 0
  1100. while go and iter<=10:
  1101. iter += 1
  1102. go = 0
  1103. if do_abf or do_abs:
  1104. valueStep, T, fuzz = self._getValueStepAndTicks(valueMin, valueMax, cache)
  1105. if do_abf:
  1106. i0 = valueStep*abf[0]
  1107. i1 = valueStep*abf[1]
  1108. else:
  1109. i0 = i1 = 0
  1110. if do_abs:
  1111. sf = (valueMax-valueMin)/aL
  1112. i0 = max(i0,abS[0]*sf)
  1113. i1 = max(i1,abS[1]*sf)
  1114. if rrn: v = T[0]
  1115. else: v = valueMin
  1116. u = cMin-i0
  1117. if (abfiz[0] or abs(v)>fuzz) and v>=u+fuzz:
  1118. valueMin = u
  1119. go = 1
  1120. if rrx: v = T[-1]
  1121. else: v = valueMax
  1122. u = cMax+i1
  1123. if (abfiz[1] or abs(v)>fuzz) and v<=u-fuzz:
  1124. valueMax = u
  1125. go = 1
  1126. if do_rr:
  1127. valueStep, T, fuzz = self._getValueStepAndTicks(valueMin, valueMax, cache)
  1128. if rrn:
  1129. if valueMin<T[0]-fuzz:
  1130. valueMin = T[0]-valueStep
  1131. go = 1
  1132. else:
  1133. go = valueMin>=T[0]+fuzz
  1134. valueMin = T[0]
  1135. if rrx:
  1136. if valueMax>T[-1]+fuzz:
  1137. valueMax = T[-1]+valueStep
  1138. go = 1
  1139. else:
  1140. go = valueMax<=T[-1]-fuzz
  1141. valueMax = T[-1]
  1142. if iter and not go:
  1143. self._computedValueStep = valueStep
  1144. else:
  1145. self._computedValueStep = None
  1146. self._valueMin = valueMin
  1147. self._valueMax = valueMax
  1148. origShiftIPC = self.origShiftIPC
  1149. origShiftMin = self.origShiftMin
  1150. if origShiftMin is not None or origShiftIPC is not None:
  1151. origShiftSpecialValue = self.origShiftSpecialValue
  1152. self._calcValueStep()
  1153. valueMax, valueMin = self._valueMax, self._valueMin
  1154. if origShiftSpecialValue is None or abs(origShiftSpecialValue-valueMin)<1e-6:
  1155. if origShiftIPC:
  1156. m = origShiftIPC*self._valueStep
  1157. else:
  1158. m = 0
  1159. if origShiftMin:
  1160. m = max(m,(valueMax-valueMin)*origShiftMin/self._length)
  1161. self._valueMin -= m
  1162. self._rangeAdjust()
  1163. def _pseudo_configure(self):
  1164. self._valueMin = self.valueMin
  1165. self._valueMax = self.valueMax
  1166. if hasattr(self,'_subTickValues'): del self._subTickValues
  1167. self._configure_end()
  1168. def _rangeAdjust(self):
  1169. """Override this if you want to alter the calculated range.
  1170. E.g. if want a minumamum range of 30% or don't want 100%
  1171. as the first point.
  1172. """
  1173. pass
  1174. def _adjustAxisTicks(self):
  1175. '''Override if you want to put slack at the ends of the axis
  1176. eg if you don't want the last tick to be at the bottom etc
  1177. '''
  1178. pass
  1179. def _calcScaleFactor(self):
  1180. """Calculate the axis' scale factor.
  1181. This should be called only *after* the axis' range is set.
  1182. Returns a number.
  1183. """
  1184. self._scaleFactor = self._length / float(self._valueMax - self._valueMin)
  1185. return self._scaleFactor
  1186. def _calcStepAndTickPositions(self):
  1187. valueStep = getattr(self,'_computedValueStep',None)
  1188. if valueStep:
  1189. del self._computedValueStep
  1190. self._valueStep = valueStep
  1191. else:
  1192. self._calcValueStep()
  1193. valueStep = self._valueStep
  1194. valueMin = self._valueMin
  1195. valueMax = self._valueMax
  1196. fuzz = 1e-8*valueStep
  1197. rangeRound = self.rangeRound
  1198. i0 = int(float(valueMin)/valueStep)
  1199. v = i0*valueStep
  1200. if rangeRound in ('both','floor'):
  1201. if v>valueMin+fuzz: i0 -= 1
  1202. elif v<valueMin-fuzz: i0 += 1
  1203. i1 = int(float(valueMax)/valueStep)
  1204. v = i1*valueStep
  1205. if rangeRound in ('both','ceiling'):
  1206. if v<valueMax-fuzz: i1 += 1
  1207. elif v>valueMax+fuzz: i1 -= 1
  1208. return valueStep,[i*valueStep for i in range(i0,i1+1)]
  1209. def _calcTickPositions(self):
  1210. return self._calcStepAndTickPositions()[1]
  1211. def _calcSubTicks(self):
  1212. if not hasattr(self,'_tickValues'):
  1213. self._pseudo_configure()
  1214. otv = self._tickValues
  1215. if not hasattr(self,'_subTickValues'):
  1216. acn = self.__class__.__name__
  1217. if acn[:11]=='NormalDateX':
  1218. iFuzz = 0
  1219. dCnv = int
  1220. else:
  1221. iFuzz = 1e-8
  1222. dCnv = lambda x:x
  1223. OTV = [tv for tv in otv if getattr(tv,'_doSubTicks',1)]
  1224. T = [].append
  1225. nst = int(self.subTickNum)
  1226. i = len(OTV)
  1227. if i<2:
  1228. self._subTickValues = []
  1229. else:
  1230. if i==2:
  1231. dst = OTV[1]-OTV[0]
  1232. elif i==3:
  1233. dst = max(OTV[1]-OTV[0],OTV[2]-OTV[1])
  1234. else:
  1235. i >>= 1
  1236. dst = OTV[i+1] - OTV[i]
  1237. fuzz = dst*iFuzz
  1238. vn = self._valueMin+fuzz
  1239. vx = self._valueMax-fuzz
  1240. if OTV[0]>vn: OTV.insert(0,OTV[0]-dst)
  1241. if OTV[-1]<vx: OTV.append(OTV[-1]+dst)
  1242. dst /= float(nst+1)
  1243. for i,x in enumerate(OTV[:-1]):
  1244. for j in range(nst):
  1245. t = x+dCnv((j+1)*dst)
  1246. if t<=vn or t>=vx: continue
  1247. T(t)
  1248. self._subTickValues = T.__self__
  1249. self._tickValues = self._subTickValues
  1250. return otv
  1251. def _calcTickmarkPositions(self):
  1252. """Calculate a list of tick positions on the axis. Returns a list of numbers."""
  1253. self._tickValues = getattr(self,'valueSteps',None)
  1254. if self._tickValues: return self._tickValues
  1255. self._tickValues = self._calcTickPositions()
  1256. self._adjustAxisTicks()
  1257. return self._tickValues
  1258. def _calcValueStep(self):
  1259. '''Calculate _valueStep for the axis or get from valueStep.'''
  1260. if self.valueStep is None:
  1261. rawRange = self._valueMax - self._valueMin
  1262. rawInterval = rawRange / min(float(self.maximumTicks-1),(float(self._length)/self.minimumTickSpacing))
  1263. self._valueStep = nextRoundNumber(rawInterval)
  1264. else:
  1265. self._valueStep = self.valueStep
  1266. def _allIntTicks(self):
  1267. return _allInt(self._tickValues)
  1268. def makeTickLabels(self):
  1269. g = Group()
  1270. if not self.visibleLabels: return g
  1271. f = self._labelTextFormat # perhaps someone already set it
  1272. if f is None:
  1273. f = self.labelTextFormat or (self._allIntTicks() and '%.0f' or _defaultLabelFormatter)
  1274. elif f is str and self._allIntTicks(): f = '%.0f'
  1275. elif hasattr(f,'calcPlaces'):
  1276. f.calcPlaces(self._tickValues)
  1277. post = self.labelTextPostFormat
  1278. scl = self.labelTextScale
  1279. pos = [self._x, self._y]
  1280. d = self._dataIndex
  1281. pos[1-d] = self._labelAxisPos()
  1282. labels = self.labels
  1283. if self.skipEndL!='none':
  1284. if self.isXAxis:
  1285. sk = self._x
  1286. else:
  1287. sk = self._y
  1288. if self.skipEndL=='start':
  1289. sk = [sk]
  1290. else:
  1291. sk = [sk,sk+self._length]
  1292. if self.skipEndL=='end':
  1293. del sk[0]
  1294. else:
  1295. sk = []
  1296. nticks = len(self._tickValues)
  1297. nticks1 = nticks - 1
  1298. for i,tick in enumerate(self._tickValues):
  1299. label = i-nticks
  1300. if label in labels:
  1301. label = labels[label]
  1302. else:
  1303. label = labels[i]
  1304. if f and label.visible:
  1305. v = self.scale(tick)
  1306. if sk:
  1307. for skv in sk:
  1308. if abs(skv-v)<1e-6:
  1309. v = None
  1310. break
  1311. if v is not None:
  1312. if scl is not None:
  1313. t = tick*scl
  1314. else:
  1315. t = tick
  1316. if isinstance(f, str): txt = f % t
  1317. elif isSeq(f):
  1318. #it's a list, use as many items as we get
  1319. if i < len(f):
  1320. txt = f[i]
  1321. else:
  1322. txt = ''
  1323. elif hasattr(f,'__call__'):
  1324. if isinstance(f,TickLabeller):
  1325. txt = f(self,t)
  1326. else:
  1327. txt = f(t)
  1328. else:
  1329. raise ValueError('Invalid labelTextFormat %s' % f)
  1330. if post: txt = post % txt
  1331. pos[d] = v
  1332. label.setOrigin(*pos)
  1333. label.setText(txt)
  1334. #special property to ensure a label doesn't project beyond the bounds of an x-axis
  1335. if self.keepTickLabelsInside:
  1336. if isinstance(self, XValueAxis): #not done yet for y axes
  1337. a_x = self._x
  1338. if not i: #first one
  1339. x0, y0, x1, y1 = label.getBounds()
  1340. if x0 < a_x:
  1341. label = label.clone(dx=label.dx + a_x - x0)
  1342. if i==nticks1: #final one
  1343. a_x1 = a_x +self._length
  1344. x0, y0, x1, y1 = label.getBounds()
  1345. if x1 > a_x1:
  1346. label=label.clone(dx=label.dx-x1+a_x1)
  1347. g.add(label)
  1348. return g
  1349. def scale(self, value):
  1350. """Converts a numeric value to a plotarea position.
  1351. The chart first configures the axis, then asks it to
  1352. """
  1353. assert self._configured, "Axis cannot scale numbers before it is configured"
  1354. if value is None: value = 0
  1355. #this could be made more efficient by moving the definition of org and sf into the configuration
  1356. org = (self._x, self._y)[self._dataIndex]
  1357. sf = self._scaleFactor
  1358. if self.reverseDirection:
  1359. sf = -sf
  1360. org += self._length
  1361. return org + sf*(value - self._valueMin)
  1362. class XValueAxis(_XTicks,ValueAxis):
  1363. "X/value axis"
  1364. _attrMap = AttrMap(BASE=ValueAxis,
  1365. tickUp = AttrMapValue(isNumber,
  1366. desc='Tick length up the axis.'),
  1367. tickDown = AttrMapValue(isNumber,
  1368. desc='Tick length down the axis.'),
  1369. joinAxis = AttrMapValue(None,
  1370. desc='Join both axes if true.'),
  1371. joinAxisMode = AttrMapValue(OneOf('bottom', 'top', 'value', 'points', None),
  1372. desc="Mode used for connecting axis ('bottom', 'top', 'value', 'points', None)."),
  1373. joinAxisPos = AttrMapValue(isNumberOrNone,
  1374. desc='Position at which to join with other axis.'),
  1375. )
  1376. # Indicate the dimension of the data we're interested in.
  1377. _dataIndex = 0
  1378. def __init__(self,**kw):
  1379. ValueAxis.__init__(self,**kw)
  1380. self.labels.boxAnchor = 'n'
  1381. self.labels.dx = 0
  1382. self.labels.dy = -5
  1383. self.tickUp = 0
  1384. self.tickDown = 5
  1385. self.joinAxis = None
  1386. self.joinAxisMode = None
  1387. self.joinAxisPos = None
  1388. def demo(self):
  1389. self.setPosition(20, 50, 150)
  1390. self.configure([(10,20,30,40,50)])
  1391. d = Drawing(200, 100)
  1392. d.add(self)
  1393. return d
  1394. def joinToAxis(self, yAxis, mode='bottom', pos=None):
  1395. "Join with y-axis using some mode."
  1396. _assertYAxis(yAxis)
  1397. if mode == 'bottom':
  1398. self._y = yAxis._y * 1.0
  1399. elif mode == 'top':
  1400. self._y = (yAxis._y + yAxis._length) * 1.0
  1401. elif mode == 'value':
  1402. self._y = yAxis.scale(pos) * 1.0
  1403. elif mode == 'points':
  1404. self._y = pos * 1.0
  1405. def _joinToAxis(self):
  1406. ja = self.joinAxis
  1407. if ja:
  1408. jam = self.joinAxisMode or 'bottom'
  1409. if jam in ('bottom', 'top'):
  1410. self.joinToAxis(ja, mode=jam)
  1411. elif jam in ('value', 'points'):
  1412. self.joinToAxis(ja, mode=jam, pos=self.joinAxisPos)
  1413. def makeAxis(self):
  1414. g = Group()
  1415. self._joinToAxis()
  1416. if not self.visibleAxis: return g
  1417. axis = Line(self._x-self.loLLen, self._y, self._x + self._length+self.hiLLen, self._y)
  1418. axis.strokeColor = self.strokeColor
  1419. axis.strokeWidth = self.strokeWidth
  1420. axis.strokeDashArray = self.strokeDashArray
  1421. g.add(axis)
  1422. return g
  1423. #additional utilities to help specify calendar dates on which tick marks
  1424. #are to be plotted. After some thought, when the magic algorithm fails,
  1425. #we can let them specify a number of days-of-the-year to tick in any given
  1426. #year.
  1427. #################################################################################
  1428. #
  1429. # Preliminary support objects/functions for the axis used in time series charts
  1430. #
  1431. #################################################################################
  1432. _months = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec']
  1433. _maxDays = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
  1434. def parseDayAndMonth(dmstr):
  1435. """This accepts and validates strings like "31-Dec" i.e. dates
  1436. of no particular year. 29 Feb is allowed. These can be used
  1437. for recurring dates. It returns a (dd, mm) pair where mm is the
  1438. month integer. If the text is not valid it raises an error.
  1439. """
  1440. dstr, mstr = dmstr.split('-')
  1441. dd = int(dstr)
  1442. mstr = mstr.lower()
  1443. mm = _months.index(mstr) + 1
  1444. assert dd <= _maxDays[mm-1]
  1445. return (dd, mm)
  1446. class _isListOfDaysAndMonths(Validator):
  1447. """This accepts and validates lists of strings like "31-Dec" i.e. dates
  1448. of no particular year. 29 Feb is allowed. These can be used
  1449. for recurring dates.
  1450. """
  1451. def test(self,x):
  1452. if isSeq(x):
  1453. answer = True
  1454. for element in x:
  1455. try:
  1456. dd, mm = parseDayAndMonth(element)
  1457. except:
  1458. answer = False
  1459. return answer
  1460. else:
  1461. return False
  1462. def normalize(self,x):
  1463. #we store them as presented, it's the most presentable way
  1464. return x
  1465. isListOfDaysAndMonths = _isListOfDaysAndMonths()
  1466. _NDINTM = 1,2,3,6,12,24,60,120,180,240,300,360,420,480,540,600,720,840,960,1080,1200,2400
  1467. class NormalDateXValueAxis(XValueAxis):
  1468. """An X axis applying additional rules.
  1469. Depending on the data and some built-in rules, the axis
  1470. displays normalDate values as nicely formatted dates.
  1471. The client chart should have NormalDate X values.
  1472. """
  1473. _attrMap = AttrMap(BASE = XValueAxis,
  1474. bottomAxisLabelSlack = AttrMapValue(isNumber, desc="Fractional amount used to adjust label spacing"),
  1475. niceMonth = AttrMapValue(isBoolean, desc="Flag for displaying months 'nicely'."),
  1476. forceEndDate = AttrMapValue(isBoolean, desc='Flag for enforced displaying of last date value.'),
  1477. forceFirstDate = AttrMapValue(isBoolean, desc='Flag for enforced displaying of first date value.'),
  1478. forceDatesEachYear = AttrMapValue(isListOfDaysAndMonths, desc='List of dates in format "31-Dec",' +
  1479. '"1-Jan". If present they will always be used for tick marks in the current year, rather ' +
  1480. 'than the dates chosen by the automatic algorithm. Hyphen compulsory, case of month optional.'),
  1481. xLabelFormat = AttrMapValue(None, desc="Label format string (e.g. '{mm}/{yy}') or function."),
  1482. dayOfWeekName = AttrMapValue(SequenceOf(isString,emptyOK=0,lo=7,hi=7), desc='Weekday names.'),
  1483. monthName = AttrMapValue(SequenceOf(isString,emptyOK=0,lo=12,hi=12), desc='Month names.'),
  1484. dailyFreq = AttrMapValue(isBoolean, desc='True if we are to assume daily data to be ticked at end of month.'),
  1485. specifiedTickDates = AttrMapValue(NoneOr(SequenceOf(isNormalDate)), desc='Actual tick values to use; no calculations done'),
  1486. specialTickClear = AttrMapValue(isBoolean, desc='clear rather than delete close ticks when forced first/end dates'),
  1487. skipGrid = AttrMapValue(OneOf('none','top','both','bottom'),"grid lines to skip top bottom both none"),
  1488. )
  1489. _valueClass = normalDate.ND
  1490. def __init__(self,**kw):
  1491. XValueAxis.__init__(self,**kw)
  1492. # some global variables still used...
  1493. self.bottomAxisLabelSlack = 0.1
  1494. self.niceMonth = 1
  1495. self.forceEndDate = 0
  1496. self.forceFirstDate = 0
  1497. self.forceDatesEachYear = []
  1498. self.dailyFreq = 0
  1499. self.xLabelFormat = "{mm}/{yy}"
  1500. self.dayOfWeekName = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
  1501. self.monthName = ['January', 'February', 'March', 'April', 'May', 'June', 'July',
  1502. 'August', 'September', 'October', 'November', 'December']
  1503. self.specialTickClear = 0
  1504. self.valueSteps = self.specifiedTickDates = None
  1505. def _scalar2ND(self, x):
  1506. "Convert a scalar to a NormalDate value."
  1507. d = self._valueClass()
  1508. d.normalize(x)
  1509. return d
  1510. def _dateFormatter(self, v):
  1511. "Create a formatted label for some value."
  1512. if not isinstance(v,normalDate.NormalDate):
  1513. v = self._scalar2ND(v)
  1514. d, m = normalDate._dayOfWeekName, normalDate._monthName
  1515. try:
  1516. normalDate._dayOfWeekName, normalDate._monthName = self.dayOfWeekName, self.monthName
  1517. return v.formatMS(self.xLabelFormat)
  1518. finally:
  1519. normalDate._dayOfWeekName, normalDate._monthName = d, m
  1520. def _xAxisTicker(self, xVals):
  1521. """Complex stuff...
  1522. Needs explanation...
  1523. Yes please says Andy :-(. Modified on 19 June 2006 to attempt to allow
  1524. a mode where one can specify recurring days and months.
  1525. """
  1526. VC = self._valueClass
  1527. axisLength = self._length
  1528. formatter = self._dateFormatter
  1529. if isinstance(formatter,TickLabeller):
  1530. def formatter(tick):
  1531. return self._dateFormatter(self,tick)
  1532. firstDate = xVals[0] if not self.valueMin else VC(self.valueMin)
  1533. endDate = xVals[-1] if not self.valueMax else VC(self.valueMax)
  1534. labels = self.labels
  1535. fontName, fontSize, leading = labels.fontName, labels.fontSize, labels.leading
  1536. textAnchor, boxAnchor, angle = labels.textAnchor, labels.boxAnchor, labels.angle
  1537. RBL = _textBoxLimits(formatter(firstDate).split('\n'),fontName,
  1538. fontSize,leading or 1.2*fontSize,textAnchor,boxAnchor)
  1539. RBL = _rotatedBoxLimits(RBL[0],RBL[1],RBL[2],RBL[3], angle)
  1540. xLabelW = RBL[1]-RBL[0]
  1541. xLabelH = RBL[3]-RBL[2]
  1542. w = max(xLabelW,labels.width or 0,self.minimumTickSpacing)
  1543. W = w+w*self.bottomAxisLabelSlack
  1544. ticks = []
  1545. labels = []
  1546. maximumTicks = self.maximumTicks
  1547. if self.specifiedTickDates:
  1548. ticks = [VC(x) for x in self.specifiedTickDates]
  1549. labels = [formatter(d) for d in ticks]
  1550. if self.forceFirstDate and firstDate==ticks[0] and (axisLength/float(ticks[-1]-ticks[0]))*(ticks[1]-ticks[0])<=W:
  1551. if self.specialTickClear:
  1552. labels[1] = ''
  1553. else:
  1554. del ticks[1], labels[1]
  1555. if self.forceEndDate and endDate==ticks[-1] and (axisLength/float(ticks[-1]-ticks[0]))*(ticks[-1]-ticks[-2])<=W:
  1556. if self.specialTickClear:
  1557. labels[-2] = ''
  1558. else:
  1559. del ticks[-2], labels[-2]
  1560. return ticks, labels
  1561. #AR 20060619 - first we try the approach where the user has explicitly
  1562. #specified the days of year to be ticked. Other explicit routes may
  1563. #be added.
  1564. if self.forceDatesEachYear:
  1565. forcedPartialDates = list(map(parseDayAndMonth, self.forceDatesEachYear))
  1566. #generate the list of dates in the range.
  1567. #print 'dates range from %s to %s' % (firstDate, endDate)
  1568. firstYear = firstDate.year()
  1569. lastYear = endDate.year()
  1570. ticks = []
  1571. labels = []
  1572. yyyy = firstYear
  1573. #generate all forced dates between the year it starts and the year it
  1574. #ends, adding them if within range.
  1575. while yyyy <= lastYear:
  1576. for (dd, mm) in forcedPartialDates:
  1577. theDate = normalDate.ND((yyyy, mm, dd))
  1578. if theDate >= firstDate and theDate <= endDate:
  1579. ticks.append(theDate)
  1580. labels.append(formatter(theDate))
  1581. yyyy += 1
  1582. #first and last may still be forced in.
  1583. if self.forceFirstDate and firstDate!=ticks[0]:
  1584. ticks.insert(0, firstDate)
  1585. labels.insert(0,formatter(firstDate))
  1586. if (axisLength/float(ticks[-1]-ticks[0]))*(ticks[1]-ticks[0])<=W:
  1587. if self.specialTickClear:
  1588. labels[1] = ''
  1589. else:
  1590. del ticks[1], labels[1]
  1591. if self.forceEndDate and endDate!=ticks[-1]:
  1592. ticks.append(endDate)
  1593. labels.append(formatter(endDate))
  1594. if (axisLength/float(ticks[-1]-ticks[0]))*(ticks[-1]-ticks[-2])<=W:
  1595. if self.specialTickClear:
  1596. labels[-2] = ''
  1597. else:
  1598. del ticks[-2], labels[-2]
  1599. #print 'xVals found on forced dates =', ticks
  1600. return ticks, labels
  1601. def addTick(i, xVals=xVals, formatter=formatter, ticks=ticks, labels=labels):
  1602. ticks.insert(0,xVals[i])
  1603. labels.insert(0,formatter(xVals[i]))
  1604. n = len(xVals)
  1605. #otherwise, we apply the 'magic algorithm...' which looks for nice spacing
  1606. #based on the size and separation of the labels.
  1607. for d in _NDINTM:
  1608. k = n/d
  1609. if k<=maximumTicks and k*W <= axisLength:
  1610. i = n-1
  1611. if self.niceMonth:
  1612. j = endDate.month() % (d<=12 and d or 12)
  1613. if j:
  1614. if self.forceEndDate:
  1615. addTick(i)
  1616. ticks[0]._doSubTicks=0
  1617. i -= j
  1618. #weird first date ie not at end of month
  1619. try:
  1620. wfd = firstDate.month() == xVals[1].month()
  1621. except:
  1622. wfd = 0
  1623. while i>=wfd:
  1624. addTick(i)
  1625. i -= d
  1626. if self.forceFirstDate and ticks[0]!=firstDate:
  1627. addTick(0)
  1628. ticks[0]._doSubTicks=0
  1629. if (axisLength/float(ticks[-1]-ticks[0]))*(ticks[1]-ticks[0])<=W:
  1630. if self.specialTickClear:
  1631. labels[1] = ''
  1632. else:
  1633. del ticks[1], labels[1]
  1634. if self.forceEndDate and self.niceMonth and j:
  1635. if (axisLength/float(ticks[-1]-ticks[0]))*(ticks[-1]-ticks[-2])<=W:
  1636. if self.specialTickClear:
  1637. labels[-2] = ''
  1638. else:
  1639. del ticks[-2], labels[-2]
  1640. try:
  1641. if labels[0] and labels[0]==labels[1]:
  1642. del ticks[1], labels[1]
  1643. except IndexError:
  1644. pass
  1645. return ticks, labels
  1646. raise ValueError('Problem selecting NormalDate value axis tick positions')
  1647. def _convertXV(self,data):
  1648. '''Convert all XValues to a standard normalDate type'''
  1649. VC = self._valueClass
  1650. for D in data:
  1651. for i in range(len(D)):
  1652. x, y = D[i]
  1653. if not isinstance(x,VC):
  1654. D[i] = (VC(x),y)
  1655. def _getStepsAndLabels(self,xVals):
  1656. if self.dailyFreq:
  1657. xEOM = []
  1658. pm = 0
  1659. px = xVals[0]
  1660. for x in xVals:
  1661. m = x.month()
  1662. if pm!=m:
  1663. if pm: xEOM.append(px)
  1664. pm = m
  1665. px = x
  1666. px = xVals[-1]
  1667. if xEOM[-1]!=x: xEOM.append(px)
  1668. steps, labels = self._xAxisTicker(xEOM)
  1669. else:
  1670. steps, labels = self._xAxisTicker(xVals)
  1671. return steps, labels
  1672. def configure(self, data):
  1673. self._convertXV(data)
  1674. xVals = set()
  1675. for x in data:
  1676. for dv in x:
  1677. xVals.add(dv[0])
  1678. xVals = list(xVals)
  1679. xVals.sort()
  1680. VC = self._valueClass
  1681. steps,labels = self._getStepsAndLabels(xVals)
  1682. valueMin, valueMax = self.valueMin, self.valueMax
  1683. valueMin = xVals[0] if valueMin is None else VC(valueMin)
  1684. valueMax = xVals[-1] if valueMax is None else VC(valueMax)
  1685. self._valueMin, self._valueMax = valueMin, valueMax
  1686. self._tickValues = steps
  1687. self._labelTextFormat = labels
  1688. self._scaleFactor = self._length / float(valueMax - valueMin)
  1689. self._tickValues = steps
  1690. self._configured = 1
  1691. class YValueAxis(_YTicks,ValueAxis):
  1692. "Y/value axis"
  1693. _attrMap = AttrMap(BASE=ValueAxis,
  1694. tickLeft = AttrMapValue(isNumber,
  1695. desc='Tick length left of the axis.'),
  1696. tickRight = AttrMapValue(isNumber,
  1697. desc='Tick length right of the axis.'),
  1698. joinAxis = AttrMapValue(None,
  1699. desc='Join both axes if true.'),
  1700. joinAxisMode = AttrMapValue(OneOf(('left', 'right', 'value', 'points', None)),
  1701. desc="Mode used for connecting axis ('left', 'right', 'value', 'points', None)."),
  1702. joinAxisPos = AttrMapValue(isNumberOrNone,
  1703. desc='Position at which to join with other axis.'),
  1704. )
  1705. # Indicate the dimension of the data we're interested in.
  1706. _dataIndex = 1
  1707. def __init__(self):
  1708. ValueAxis.__init__(self)
  1709. self.labels.boxAnchor = 'e'
  1710. self.labels.dx = -5
  1711. self.labels.dy = 0
  1712. self.tickRight = 0
  1713. self.tickLeft = 5
  1714. self.joinAxis = None
  1715. self.joinAxisMode = None
  1716. self.joinAxisPos = None
  1717. def demo(self):
  1718. data = [(10, 20, 30, 42)]
  1719. self.setPosition(100, 10, 80)
  1720. self.configure(data)
  1721. drawing = Drawing(200, 100)
  1722. drawing.add(self)
  1723. return drawing
  1724. def joinToAxis(self, xAxis, mode='left', pos=None):
  1725. "Join with x-axis using some mode."
  1726. _assertXAxis(xAxis)
  1727. if mode == 'left':
  1728. self._x = xAxis._x * 1.0
  1729. elif mode == 'right':
  1730. self._x = (xAxis._x + xAxis._length) * 1.0
  1731. elif mode == 'value':
  1732. self._x = xAxis.scale(pos) * 1.0
  1733. elif mode == 'points':
  1734. self._x = pos * 1.0
  1735. def _joinToAxis(self):
  1736. ja = self.joinAxis
  1737. if ja:
  1738. jam = self.joinAxisMode
  1739. if jam in ('left', 'right'):
  1740. self.joinToAxis(ja, mode=jam)
  1741. elif jam in ('value', 'points'):
  1742. self.joinToAxis(ja, mode=jam, pos=self.joinAxisPos)
  1743. def makeAxis(self):
  1744. g = Group()
  1745. self._joinToAxis()
  1746. if not self.visibleAxis: return g
  1747. axis = Line(self._x, self._y-self.loLLen, self._x, self._y + self._length+self.hiLLen)
  1748. axis.strokeColor = self.strokeColor
  1749. axis.strokeWidth = self.strokeWidth
  1750. axis.strokeDashArray = self.strokeDashArray
  1751. g.add(axis)
  1752. return g
  1753. class TimeValueAxis:
  1754. _mc = 60
  1755. _hc = 60*_mc
  1756. _dc = 24*_hc
  1757. def __init__(self,*args,**kwds):
  1758. if not self.labelTextFormat:
  1759. self.labelTextFormat = self.timeLabelTextFormatter
  1760. self._saved_tickInfo = {}
  1761. def _calcValueStep(self):
  1762. '''Calculate _valueStep for the axis or get from valueStep.'''
  1763. if self.valueStep is None:
  1764. rawRange = self._valueMax - self._valueMin
  1765. rawInterval = rawRange / min(float(self.maximumTicks-1),(float(self._length)/self.minimumTickSpacing))
  1766. #here's where we try to choose the correct value for the unit
  1767. if rawInterval >= self._dc:
  1768. d = self._dc
  1769. self._unit = 'days'
  1770. elif rawInterval >= self._hc:
  1771. d = self._hc
  1772. self._unit = 'hours'
  1773. elif rawInterval >= self._mc:
  1774. d = self._mc
  1775. self._unit = 'minutes'
  1776. else:
  1777. d = 1
  1778. self._unit = 'seconds'
  1779. self._unitd = d
  1780. if d>1:
  1781. rawInterval = int(rawInterval/d)
  1782. self._valueStep = nextRoundNumber(rawInterval) * d
  1783. else:
  1784. self._valueStep = self.valueStep
  1785. def timeLabelTextFormatter(self,val):
  1786. u = self._unitd
  1787. k = (u,tuple(self._tickValues))
  1788. if k in self._saved_tickInfo:
  1789. fmt = self._saved_tickInfo[k]
  1790. else:
  1791. uf = float(u)
  1792. tv = [v/uf for v in self._tickValues]
  1793. s = self._unit[0]
  1794. if _allInt(tv):
  1795. fmt = lambda x, uf=uf, s=s: '%.0f%s' % (x/uf,s)
  1796. else:
  1797. stv = ['%.10f' % v for v in tv]
  1798. stvl = max((len(v.rstrip('0'))-v.index('.')-1) for v in stv)
  1799. if u==1:
  1800. fmt = lambda x,uf=uf,fmt='%%.%dfs' % stvl: fmt % (x/uf)
  1801. else:
  1802. #see if we can represent fractions
  1803. fm = 24 if u==self._dc else 60
  1804. fv = [(v - int(v))*fm for v in tv]
  1805. if _allInt(fv):
  1806. s1 = 'h' if u==self._dc else ('m' if u==self._mc else 's')
  1807. fmt = lambda x,uf=uf,fm=fm, fmt='%%d%s%%d%%s' % (s,s1): fmt % (int(x/uf),int((x/uf - int(x/uf))*fm))
  1808. else:
  1809. fmt = lambda x,uf=uf,fmt='%%.%df%s' % (stvl,s): fmt % (x/uf)
  1810. self._saved_tickInfo[k] = fmt
  1811. return fmt(val)
  1812. class XTimeValueAxis(TimeValueAxis,XValueAxis):
  1813. def __init__(self,*args,**kwds):
  1814. XValueAxis.__init__(self,*args,**kwds)
  1815. TimeValueAxis.__init__(self,*args,**kwds)
  1816. class AdjYValueAxis(YValueAxis):
  1817. """A Y-axis applying additional rules.
  1818. Depending on the data and some built-in rules, the axis
  1819. may choose to adjust its range and origin.
  1820. """
  1821. _attrMap = AttrMap(BASE = YValueAxis,
  1822. leftAxisPercent = AttrMapValue(isBoolean, desc='When true add percent sign to label values.'),
  1823. leftAxisOrigShiftIPC = AttrMapValue(isNumber, desc='Lowest label shift interval ratio.'),
  1824. leftAxisOrigShiftMin = AttrMapValue(isNumber, desc='Minimum amount to shift.'),
  1825. leftAxisSkipLL0 = AttrMapValue(EitherOr((isBoolean,isListOfNumbers)), desc='Skip/Keep lowest tick label when true/false.\nOr skiplist'),
  1826. labelVOffset = AttrMapValue(isNumber, desc='add this to the labels'),
  1827. )
  1828. def __init__(self,**kw):
  1829. YValueAxis.__init__(self,**kw)
  1830. self.requiredRange = 30
  1831. self.leftAxisPercent = 1
  1832. self.leftAxisOrigShiftIPC = 0.15
  1833. self.leftAxisOrigShiftMin = 12
  1834. self.leftAxisSkipLL0 = self.labelVOffset = 0
  1835. self.valueSteps = None
  1836. def _rangeAdjust(self):
  1837. "Adjusts the value range of the axis."
  1838. from reportlab.graphics.charts.utils import find_good_grid, ticks
  1839. y_min, y_max = self._valueMin, self._valueMax
  1840. m = self.maximumTicks
  1841. n = list(filter(lambda x,m=m: x<=m,[4,5,6,7,8,9]))
  1842. if not n: n = [m]
  1843. valueStep, requiredRange = self.valueStep, self.requiredRange
  1844. if requiredRange and y_max - y_min < requiredRange:
  1845. y1, y2 = find_good_grid(y_min, y_max,n=n,grid=valueStep)[:2]
  1846. if y2 - y1 < requiredRange:
  1847. ym = (y1+y2)*0.5
  1848. y1 = min(ym-requiredRange*0.5,y_min)
  1849. y2 = max(ym+requiredRange*0.5,y_max)
  1850. if y_min>=100 and y1<100:
  1851. y2 = y2 + 100 - y1
  1852. y1 = 100
  1853. elif y_min>=0 and y1<0:
  1854. y2 = y2 - y1
  1855. y1 = 0
  1856. self._valueMin, self._valueMax = y1, y2
  1857. T, L = ticks(self._valueMin, self._valueMax, split=1, n=n, percent=self.leftAxisPercent,grid=valueStep, labelVOffset=self.labelVOffset)
  1858. abf = self.avoidBoundFrac
  1859. if abf:
  1860. i1 = (T[1]-T[0])
  1861. if not isSeq(abf):
  1862. i0 = i1 = i1*abf
  1863. else:
  1864. i0 = i1*abf[0]
  1865. i1 = i1*abf[1]
  1866. _n = getattr(self,'_cValueMin',T[0])
  1867. _x = getattr(self,'_cValueMax',T[-1])
  1868. if _n - T[0] < i0: self._valueMin = self._valueMin - i0
  1869. if T[-1]-_x < i1: self._valueMax = self._valueMax + i1
  1870. T, L = ticks(self._valueMin, self._valueMax, split=1, n=n, percent=self.leftAxisPercent,grid=valueStep, labelVOffset=self.labelVOffset)
  1871. self._valueMin = T[0]
  1872. self._valueMax = T[-1]
  1873. self._tickValues = T
  1874. if self.labelTextFormat is None:
  1875. self._labelTextFormat = L
  1876. else:
  1877. self._labelTextFormat = self.labelTextFormat
  1878. if abs(self._valueMin-100)<1e-6:
  1879. self._calcValueStep()
  1880. vMax, vMin = self._valueMax, self._valueMin
  1881. m = max(self.leftAxisOrigShiftIPC*self._valueStep,
  1882. (vMax-vMin)*self.leftAxisOrigShiftMin/self._length)
  1883. self._valueMin = self._valueMin - m
  1884. if self.leftAxisSkipLL0:
  1885. if isSeq(self.leftAxisSkipLL0):
  1886. for x in self.leftAxisSkipLL0:
  1887. try:
  1888. L[x] = ''
  1889. except IndexError:
  1890. pass
  1891. L[0] = ''
  1892. class LogValueAxis(ValueAxis):
  1893. def _calcScaleFactor(self):
  1894. """Calculate the axis' scale factor.
  1895. This should be called only *after* the axis' range is set.
  1896. Returns a number.
  1897. """
  1898. self._scaleFactor = self._length / float(
  1899. math_log10(self._valueMax) - math_log10(self._valueMin))
  1900. return self._scaleFactor
  1901. def _setRange(self,dataSeries):
  1902. valueMin = self.valueMin
  1903. valueMax = self.valueMax
  1904. aMin = _findMin(dataSeries,self._dataIndex,0)
  1905. aMax = _findMax(dataSeries,self._dataIndex,0)
  1906. if valueMin is None: valueMin = aMin
  1907. if valueMax is None: valueMax = aMax
  1908. if valueMin>valueMax:
  1909. raise ValueError('%s: valueMin=%r should not be greater than valueMax=%r!' % (self.__class__.__name__valueMin, valueMax))
  1910. if valueMin<=0:
  1911. raise ValueError('%s: valueMin=%r negative values are not allowed!' % valueMin)
  1912. abS = self.avoidBoundSpace
  1913. if abS:
  1914. lMin = math_log10(aMin)
  1915. lMax = math_log10(aMax)
  1916. if not isSeq(abS): abS = abS, abS
  1917. a0 = abS[0] or 0
  1918. a1 = abS[1] or 0
  1919. L = self._length - (a0 + a1)
  1920. sf = (lMax-lMin)/float(L)
  1921. lMin -= a0*sf
  1922. lMax += a1*sf
  1923. valueMin = min(valueMin,10**lMin)
  1924. valueMax = max(valueMax,10**lMax)
  1925. self._valueMin = valueMin
  1926. self._valueMax = valueMax
  1927. def _calcTickPositions(self):
  1928. #self._calcValueStep()
  1929. valueMin = cMin = math_log10(self._valueMin)
  1930. valueMax = cMax = math_log10(self._valueMax)
  1931. rr = self.rangeRound
  1932. if rr:
  1933. if rr in ('both','ceiling'):
  1934. i = int(valueMax)
  1935. valueMax = i + 1 if i<valueMax else i
  1936. if rr in ('both','floor'):
  1937. i = int(valueMin)
  1938. valueMin = i - 1 if i>valueMin else i
  1939. T = [].append
  1940. tv = int(valueMin)
  1941. if tv<valueMin: tv += 1
  1942. n = int(valueMax) - tv + 1
  1943. i = max(int(n/self.maximumTicks),1)
  1944. if i*n>self.maximumTicks: i += 1
  1945. self._powerInc = i
  1946. while True:
  1947. if tv>valueMax: break
  1948. if tv>=valueMin: T(10**tv)
  1949. tv += i
  1950. if valueMin!=cMin: self._valueMin = 10**valueMin
  1951. if valueMax!=cMax: self._valueMax = 10**valueMax
  1952. return T.__self__
  1953. def _calcSubTicks(self):
  1954. if not hasattr(self,'_tickValues'):
  1955. self._pseudo_configure()
  1956. otv = self._tickValues
  1957. if not hasattr(self,'_subTickValues'):
  1958. T = [].append
  1959. valueMin = math_log10(self._valueMin)
  1960. valueMax = math_log10(self._valueMax)+1
  1961. tv = round(valueMin)
  1962. i = self._powerInc
  1963. if i==1:
  1964. fac = 10 / float(self.subTickNum)
  1965. start = 1
  1966. if self.subTickNum == 10: start = 2
  1967. while tv < valueMax:
  1968. for j in range(start,self.subTickNum):
  1969. v = fac*j*(10**tv)
  1970. if v > self._valueMin and v < self._valueMax:
  1971. T(v)
  1972. tv += i
  1973. else:
  1974. ng = min(self.subTickNum+1,i-1)
  1975. while ng:
  1976. if (i % ng)==0:
  1977. i /= ng
  1978. break
  1979. ng -= 1
  1980. else:
  1981. i = 1
  1982. tv = round(valueMin)
  1983. while True:
  1984. v = 10**tv
  1985. if v >= self._valueMax: break
  1986. if v not in otv:
  1987. T(v)
  1988. tv += i
  1989. self._subTickValues = T.__self__
  1990. self._tickValues = self._subTickValues
  1991. return otv
  1992. class LogAxisTickLabeller(TickLabeller):
  1993. def __call__(self,axis,value):
  1994. e = math_log10(value)
  1995. e = int(e-0.001 if e<0 else e+0.001)
  1996. if e==0: return '1'
  1997. if e==1: return '10'
  1998. return '10<sup>%s</sup>' % e
  1999. class LogAxisLabellingSetup:
  2000. def __init__(self):
  2001. if DirectDrawFlowable is not None:
  2002. self.labels = TypedPropertyCollection(XLabel)
  2003. if self._dataIndex==1:
  2004. self.labels.boxAnchor = 'e'
  2005. self.labels.dx = -5
  2006. self.labels.dy = 0
  2007. else:
  2008. self.labels.boxAnchor = 'n'
  2009. self.labels.dx = 0
  2010. self.labels.dy = -5
  2011. self.labelTextFormat = LogAxisTickLabeller()
  2012. else:
  2013. self.labelTextFormat = "%.0e"
  2014. class LogXValueAxis(LogValueAxis,LogAxisLabellingSetup,XValueAxis):
  2015. _attrMap = AttrMap(BASE=XValueAxis)
  2016. def __init__(self):
  2017. XValueAxis.__init__(self)
  2018. LogAxisLabellingSetup.__init__(self)
  2019. def scale(self, value):
  2020. """Converts a numeric value to a Y position.
  2021. The chart first configures the axis, then asks it to
  2022. work out the x value for each point when plotting
  2023. lines or bars. You could override this to do
  2024. logarithmic axes.
  2025. """
  2026. msg = "Axis cannot scale numbers before it is configured"
  2027. assert self._configured, msg
  2028. if value is None:
  2029. value = 0
  2030. if value == 0.:
  2031. return self._x - self._scaleFactor * math_log10(self._valueMin)
  2032. return self._x + self._scaleFactor * (math_log10(value) - math_log10(self._valueMin))
  2033. class LogYValueAxis(LogValueAxis,LogAxisLabellingSetup,YValueAxis):
  2034. _attrMap = AttrMap(BASE=YValueAxis)
  2035. def __init__(self):
  2036. YValueAxis.__init__(self)
  2037. LogAxisLabellingSetup.__init__(self)
  2038. def scale(self, value):
  2039. """Converts a numeric value to a Y position.
  2040. The chart first configures the axis, then asks it to
  2041. work out the x value for each point when plotting
  2042. lines or bars. You could override this to do
  2043. logarithmic axes.
  2044. """
  2045. msg = "Axis cannot scale numbers before it is configured"
  2046. assert self._configured, msg
  2047. if value is None:
  2048. value = 0
  2049. if value == 0.:
  2050. return self._y - self._scaleFactor * math_log10(self._valueMin)
  2051. return self._y + self._scaleFactor * (math_log10(value) - math_log10(self._valueMin))