graph style + data reorganization completed
[PyX/mjg.git] / pyx / graph / axis / axis.py
blob587da0a071b1f7783fdb06f31bfd3e5c0519d44c
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2004 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
26 import math
27 from pyx import attr, helper
28 from pyx.graph.axis import painter, parter, rater, texter, tick
31 class _Imap:
32 """interface definition of a map
33 maps convert a value into another value by bijective transformation f"""
35 def convert(self, x):
36 "returns f(x)"
38 def invert(self, y):
39 "returns f^-1(y) where f^-1 is the inverse transformation (x=f^-1(f(x)) for all x)"
41 def setbasepoints(self, basepoints):
42 """set basepoints for the convertions
43 basepoints are tuples (x, y) with y == f(x) and x == f^-1(y)
44 the number of basepoints needed might depend on the transformation
45 usually two pairs are needed like for linear maps, logarithmic maps, etc."""
48 class _linmap:
49 "linear mapping"
51 __implements__ = _Imap
53 def setbasepoints(self, basepoints):
54 self.x1 = float(basepoints[0][0])
55 self.y1 = float(basepoints[0][1])
56 self.x2 = float(basepoints[1][0])
57 self.y2 = float(basepoints[1][1])
58 self.dydx = (self.y2 - self.y1) / (self.x2 - self.x1)
59 self.dxdy = (self.x2 - self.x1) / (self.y2 - self.y1)
61 def convert(self, value):
62 return self.y1 + self.dydx * (float(value) - self.x1)
64 def invert(self, value):
65 return self.x1 + self.dxdy * (float(value) - self.y1)
68 class _logmap:
69 "logarithmic mapping"
70 __implements__ = _Imap
72 def setbasepoints(self, basepoints):
73 self.x1 = float(math.log(basepoints[0][0]))
74 self.y1 = float(basepoints[0][1])
75 self.x2 = float(math.log(basepoints[1][0]))
76 self.y2 = float(basepoints[1][1])
77 self.dydx = (self.y2 - self.y1) / (self.x2 - self.x1)
78 self.dxdy = (self.x2 - self.x1) / (self.y2 - self.y1)
80 def convert(self, value):
81 return self.y1 + self.dydx * (math.log(float(value)) - self.x1)
83 def invert(self, value):
84 return math.exp(self.x1 + self.dxdy * (float(value) - self.y1))
87 class _Iaxis:
88 """interface definition of a axis
89 - an axis should implement an convert and invert method like
90 _Imap, but this is not part of this interface definition;
91 one possibility is to mix-in a proper map class, but special
92 purpose axes might do something else
93 - an axis has the instance variable axiscanvas after the finish
94 method was called
95 - an axis might have further instance variables (title, ticks)
96 to be used in combination with appropriate axispainters"""
98 def convert(self, x):
99 "convert a value into graph coordinates"
101 def invert(self, v):
102 "invert a graph coordinate to a axis value"
104 def getrelsize(self):
105 """returns the relative size (width) of the axis
106 - for use in split axis, bar axis etc.
107 - might return None if no size is available"""
109 # TODO: describe adjustrange
110 def setrange(self, min=None, max=None):
111 """set the axis data range
112 - the type of min and max must fit to the axis
113 - min<max; the axis might be reversed, but this is
114 expressed internally only (min<max all the time)
115 - the axis might not apply the change of the range
116 (e.g. when the axis range is fixed by the user),
117 but usually the range is extended to contain the
118 given range
119 - for invalid parameters (e.g. negativ values at an
120 logarithmic axis), an exception should be raised
121 - a RuntimeError is raised, when setrange is called
122 after the finish method"""
124 def getrange(self):
125 """return data range as a tuple (min, max)
126 - min<max; the axis might be reversed, but this is
127 expressed internally only
128 - a RuntimeError exception is raised when no
129 range is available"""
131 def finish(self, axispos):
132 """finishes the axis
133 - axispos implements _Iaxispos
134 - sets the instance axiscanvas, which is insertable into the
135 graph to finally paint the axis
136 - any modification of the axis range should be disabled after
137 the finish method was called"""
138 # TODO: be more specific about exceptions
140 def createlinkaxis(self, **kwargs):
141 """create a link axis to the axis itself
142 - typically, a link axis is a axis, which share almost
143 all properties with the axis it is linked to
144 - typically, the painter gets replaced by a painter
145 which doesn't put any text to the axis"""
148 class _axis:
149 """base implementation a regular axis
150 - typical usage is to mix-in a linmap or a logmap to
151 complete the axis interface
152 - note that some methods of this class want to access a
153 parter and a rater; those attributes implementing _Iparter
154 and _Irater should be initialized by the constructors
155 of derived classes"""
157 def __init__(self, min=None, max=None, reverse=0, divisor=None,
158 title=None, painter=painter.regular(), texter=texter.mixed(),
159 density=1, maxworse=2, manualticks=[]):
160 """initializes the instance
161 - min and max fix the axis minimum and maximum, respectively;
162 they are determined by the data to be plotted, when not fixed
163 - reverse (boolean) reverses the minimum and the maximum of
164 the axis
165 - numerical divisor for the axis partitioning
166 - title is a string containing the axis title
167 - axispainter is the axis painter (should implement _Ipainter)
168 - texter is the texter (should implement _Itexter)
169 - density is a global parameter for the axis paritioning and
170 axis rating; its default is 1, but the range 0.5 to 2.5 should
171 be usefull to get less or more ticks by the automatic axis
172 partitioning
173 - maxworse is a number of trials with worse tick rating
174 before giving up (usually it should not be needed to increase
175 this value; increasing the number will slow down the automatic
176 axis partitioning considerably)
177 - manualticks and the partitioner results are mixed
178 by mergeticklists
179 - note that some methods of this class want to access a
180 parter and a rater; those attributes implementing _Iparter
181 and _Irater should be initialized by the constructors
182 of derived classes"""
183 if min is not None and max is not None and min > max:
184 min, max, reverse = max, min, not reverse
185 self.fixmin, self.fixmax, self.min, self.max, self.reverse = min is not None, max is not None, min, max, reverse
186 self.divisor = divisor
187 self.usedivisor = 0
188 self.title = title
189 self.painter = painter
190 self.texter = texter
191 self.density = density
192 self.maxworse = maxworse
193 self.manualticks = self.checkfraclist(manualticks)
194 self.axiscanvas = None
195 self._setrange()
197 def _setrange(self, min=None, max=None):
198 if not self.fixmin and min is not None and (self.min is None or min < self.min):
199 self.min = min
200 if not self.fixmax and max is not None and (self.max is None or max > self.max):
201 self.max = max
202 if None not in (self.min, self.max) and self.min != self.max:
203 if self.reverse:
204 if self.usedivisor and self.divisor is not None:
205 self.setbasepoints(((self.min/self.divisor, 1), (self.max/self.divisor, 0)))
206 else:
207 self.setbasepoints(((self.min, 1), (self.max, 0)))
208 else:
209 if self.usedivisor and self.divisor is not None:
210 self.setbasepoints(((self.min/self.divisor, 0), (self.max/self.divisor, 1)))
211 else:
212 self.setbasepoints(((self.min, 0), (self.max, 1)))
214 def _getrange(self):
215 return self.min, self.max
217 def _forcerange(self, range):
218 self.min, self.max = range
219 self._setrange()
221 def setrange(self, min=None, max=None):
222 if self.usedivisor and self.divisor is not None:
223 min = float(self.divisor) * self.divisor
224 max = float(self.divisor) * self.divisor
225 oldmin, oldmax = self.min, self.max
226 self._setrange(min, max)
227 if self.axiscanvas is not None and ((oldmin != self.min) or (oldmax != self.max)):
228 raise RuntimeError("range modification while axis was already finished")
230 zero = 0.0
232 def adjustrange(self, data, index, deltamindata=None, deltaminindex=None, deltamaxdata=None, deltamaxindex=None):
233 assert deltamindata is None or deltamaxdata is None
234 min = max = None
235 if deltamindata is not None:
236 for point, minpoint in zip(data, deltamindata):
237 try:
238 if index is not None:
239 if deltaminindex is not None:
240 value = point[index] - minpoint[deltaminindex] + self.zero
241 else:
242 value = point[index] - minpoint + self.zero
243 else:
244 if deltaminindex is not None:
245 value = point - minpoint[deltaminindex] + self.zero
246 else:
247 value = point - minpoint + self.zero
248 except:
249 pass
250 else:
251 if min is None or value < min: min = value
252 if max is None or value > max: max = value
253 elif deltamaxdata is not None:
254 for point, maxpoint in zip(data, deltamaxdata):
255 try:
256 if index is not None:
257 if deltamaxindex is not None:
258 value = point[index] + maxpoint[deltamaxindex] + self.zero
259 else:
260 value = point[index] + maxpoint + self.zero
261 else:
262 if deltamaxindex is not None:
263 value = point + maxpoint[deltamaxindex] + self.zero
264 else:
265 value = point + maxpoint + self.zero
266 except:
267 pass
268 else:
269 if min is None or value < min: min = value
270 if max is None or value > max: max = value
271 else:
272 for point in data:
273 try:
274 if index is not None:
275 value = point[index] + self.zero
276 else:
277 value = point + self.zero
278 except:
279 pass
280 else:
281 if min is None or value < min: min = value
282 if max is None or value > max: max = value
283 self.setrange(min, max)
285 def getrange(self):
286 if self.min is not None and self.max is not None:
287 if self.usedivisor and self.divisor is not None:
288 return float(self.min) / self.divisor, float(self.max) / self.divisor
289 else:
290 return self.min, self.max
292 def checkfraclist(self, fracs):
293 "orders a list of fracs, equal entries are not allowed"
294 if not len(fracs): return []
295 sorted = list(fracs)
296 sorted.sort()
297 last = sorted[0]
298 for item in sorted[1:]:
299 if last == item:
300 raise ValueError("duplicate entry found")
301 last = item
302 return sorted
304 def finish(self, axispos):
305 if self.axiscanvas is not None: return
307 # temorarily enable the axis divisor
308 self.usedivisor = 1
309 self._setrange()
311 # lesspart and morepart can be called after defaultpart;
312 # this works although some axes may share their autoparting,
313 # because the axes are processed sequentially
314 first = 1
315 if self.parter is not None:
316 min, max = self.getrange()
317 self.ticks = tick.mergeticklists(self.manualticks,
318 self.parter.defaultpart(min, max, not self.fixmin, not self.fixmax))
319 worse = 0
320 nextpart = self.parter.lesspart
321 while nextpart is not None:
322 newticks = nextpart()
323 worse += 1
324 if newticks is not None:
325 newticks = tick.mergeticklists(self.manualticks, newticks)
326 if first:
327 if len(self.ticks):
328 bestrate = self.rater.rateticks(self, self.ticks, self.density)
329 bestrate += self.rater.raterange(self.convert(self.ticks[-1])-
330 self.convert(self.ticks[0]), 1)
331 variants = [[bestrate, self.ticks]]
332 else:
333 bestrate = None
334 variants = []
335 first = 0
336 if len(newticks):
337 newrate = self.rater.rateticks(self, newticks, self.density)
338 newrate += self.rater.raterange(self.convert(newticks[-1])-
339 self.convert(newticks[0]), 1)
340 variants.append([newrate, newticks])
341 if bestrate is None or newrate < bestrate:
342 bestrate = newrate
343 worse = 0
344 if worse == self.maxworse and nextpart == self.parter.lesspart:
345 worse = 0
346 nextpart = self.parter.morepart
347 if worse == self.maxworse and nextpart == self.parter.morepart:
348 nextpart = None
349 else:
350 self.ticks =self.manualticks
352 # rating, when several choises are available
353 if not first:
354 variants.sort()
355 if self.painter is not None:
356 i = 0
357 bestrate = None
358 while i < len(variants) and (bestrate is None or variants[i][0] < bestrate):
359 saverange = self._getrange()
360 self.ticks = variants[i][1]
361 if len(self.ticks):
362 self.setrange(self.ticks[0], self.ticks[-1])
363 self.texter.labels(self.ticks)
364 ac = self.painter.paint(axispos, self)
365 ratelayout = self.rater.ratelayout(ac, self.density)
366 if ratelayout is not None:
367 variants[i][0] += ratelayout
368 variants[i].append(ac)
369 else:
370 variants[i][0] = None
371 if variants[i][0] is not None and (bestrate is None or variants[i][0] < bestrate):
372 bestrate = variants[i][0]
373 self._forcerange(saverange)
374 i += 1
375 if bestrate is None:
376 raise RuntimeError("no valid axis partitioning found")
377 variants = [variant for variant in variants[:i] if variant[0] is not None]
378 variants.sort()
379 self.ticks = variants[0][1]
380 if len(self.ticks):
381 self.setrange(self.ticks[0], self.ticks[-1])
382 self.axiscanvas = variants[0][2]
383 else:
384 self.ticks = variants[0][1]
385 self.texter.labels(self.ticks)
386 if len(self.ticks):
387 self.setrange(self.ticks[0], self.ticks[-1])
388 self.axiscanvas = painter.axiscanvas()
389 else:
390 if len(self.ticks):
391 self.setrange(self.ticks[0], self.ticks[-1])
392 self.texter.labels(self.ticks)
393 if self.painter is not None:
394 self.axiscanvas = self.painter.paint(axispos, self)
395 else:
396 self.axiscanvas = painter.axiscanvas()
398 # disable the axis divisor
399 self.usedivisor = 0
400 self._setrange()
402 def createlinkaxis(self, **args):
403 return linked(self, **args)
406 class linear(_axis, _linmap):
407 """implementation of a linear axis"""
409 __implements__ = _Iaxis
411 def __init__(self, parter=parter.autolinear(), rater=rater.linear(), **args):
412 """initializes the instance
413 - the parter attribute implements _Iparter
414 - manualticks and the partitioner results are mixed
415 by mergeticklists
416 - the rater implements _Irater and is used to rate different
417 tick lists created by the partitioner (after merging with
418 manully set ticks)
419 - futher keyword arguments are passed to _axis"""
420 _axis.__init__(self, **args)
421 if self.fixmin and self.fixmax:
422 self.relsize = self.max - self.min
423 self.parter = parter
424 self.rater = rater
426 lin = linear
429 class logarithmic(_axis, _logmap):
430 """implementation of a logarithmic axis"""
432 __implements__ = _Iaxis
434 def __init__(self, parter=parter.autologarithmic(), rater=rater.logarithmic(), **args):
435 """initializes the instance
436 - the parter attribute implements _Iparter
437 - manualticks and the partitioner results are mixed
438 by mergeticklists
439 - the rater implements _Irater and is used to rate different
440 tick lists created by the partitioner (after merging with
441 manully set ticks)
442 - futher keyword arguments are passed to _axis"""
443 _axis.__init__(self, **args)
444 if self.fixmin and self.fixmax:
445 self.relsize = math.log(self.max) - math.log(self.min)
446 self.parter = parter
447 self.rater = rater
449 log = logarithmic
452 class _linked:
453 """base for a axis linked to an already existing axis
454 - almost all properties of the axis are "copied" from the
455 axis this axis is linked to
456 - usually, linked axis are used to create an axis to an
457 existing axis with different painting properties; linked
458 axis can be used to plot an axis twice at the opposite
459 sides of a graphxy or even to share an axis between
460 different graphs!"""
462 __implements__ = _Iaxis
464 def __init__(self, linkedaxis, painter=painter.linked()):
465 """initializes the instance
466 - it gets a axis this axis is linked to
467 - it gets a painter to be used for this linked axis"""
468 self.linkedaxis = linkedaxis
469 self.painter = painter
470 self.axiscanvas = None
472 def __getattr__(self, attr):
473 """access to unkown attributes are handed over to the
474 axis this axis is linked to"""
475 return getattr(self.linkedaxis, attr)
477 def finish(self, axispos):
478 """finishes the axis
479 - instead of performing the hole finish process
480 (paritioning, rating, etc.) just a painter call
481 is performed"""
483 if self.axiscanvas is None:
484 if self.linkedaxis.axiscanvas is None:
485 raise RuntimeError("link axis finish method called before the finish method of the original axis")
486 self.axiscanvas = self.painter.paint(axispos, self)
489 class linked(_linked):
490 """a axis linked to an already existing regular axis
491 - adds divisor handling to _linked"""
493 __implements__ = _Iaxis
495 def finish(self, axispos):
496 # temporarily enable the linkedaxis divisor
497 self.linkedaxis.usedivisor = 1
498 self.linkedaxis._setrange()
500 _linked.finish(self, axispos)
502 # disable the linkedaxis divisor again
503 self.linkedaxis.usedivisor = 0
504 self.linkedaxis._setrange()
507 class split:
508 """implementation of a split axis
509 - a split axis contains several (sub-)axes with
510 non-overlapping data ranges -- between these subaxes
511 the axis is "splitted"
512 - (just to get sure: a split axis can contain other
513 split axes as its subaxes)"""
515 __implements__ = _Iaxis, painter._Iaxispos
517 def __init__(self, subaxes, splitlist=[0.5], splitdist=0.1, relsizesplitdist=1,
518 title=None, painter=painter.split()):
519 """initializes the instance
520 - subaxes is a list of subaxes
521 - splitlist is a list of graph coordinates, where the splitting
522 of the main axis should be performed; if the list isn't long enough
523 for the subaxes, missing entries are considered to be None
524 - splitdist is the size of the splitting in graph coordinates, when
525 the associated splitlist entry is not None
526 - relsizesplitdist: a None entry in splitlist means, that the
527 position of the splitting should be calculated out of the
528 relsize values of conrtibuting subaxes (the size of the
529 splitting is relsizesplitdist in values of the relsize values
530 of the axes)
531 - title is the title of the axis as a string
532 - painter is the painter of the axis; it should be specialized to
533 the split axis
534 - the relsize of the split axis is the sum of the relsizes of the
535 subaxes including the relsizesplitdist"""
536 self.subaxes = subaxes
537 self.painter = painter
538 self.title = title
539 self.splitlist = splitlist
540 for subaxis in self.subaxes:
541 subaxis.vmin = None
542 subaxis.vmax = None
543 self.subaxes[0].vmin = 0
544 self.subaxes[0].vminover = None
545 self.subaxes[-1].vmax = 1
546 self.subaxes[-1].vmaxover = None
547 for i in xrange(len(self.splitlist)):
548 if self.splitlist[i] is not None:
549 self.subaxes[i].vmax = self.splitlist[i] - 0.5*splitdist
550 self.subaxes[i].vmaxover = self.splitlist[i]
551 self.subaxes[i+1].vmin = self.splitlist[i] + 0.5*splitdist
552 self.subaxes[i+1].vminover = self.splitlist[i]
553 i = 0
554 while i < len(self.subaxes):
555 if self.subaxes[i].vmax is None:
556 j = relsize = relsize2 = 0
557 while self.subaxes[i + j].vmax is None:
558 relsize += self.subaxes[i + j].relsize + relsizesplitdist
559 j += 1
560 relsize += self.subaxes[i + j].relsize
561 vleft = self.subaxes[i].vmin
562 vright = self.subaxes[i + j].vmax
563 for k in range(i, i + j):
564 relsize2 += self.subaxes[k].relsize
565 self.subaxes[k].vmax = vleft + (vright - vleft) * relsize2 / float(relsize)
566 relsize2 += 0.5 * relsizesplitdist
567 self.subaxes[k].vmaxover = self.subaxes[k + 1].vminover = vleft + (vright - vleft) * relsize2 / float(relsize)
568 relsize2 += 0.5 * relsizesplitdist
569 self.subaxes[k+1].vmin = vleft + (vright - vleft) * relsize2 / float(relsize)
570 if i == 0 and i + j + 1 == len(self.subaxes):
571 self.relsize = relsize
572 i += j + 1
573 else:
574 i += 1
576 self.fixmin = self.subaxes[0].fixmin
577 if self.fixmin:
578 self.min = self.subaxes[0].min
579 self.fixmax = self.subaxes[-1].fixmax
580 if self.fixmax:
581 self.max = self.subaxes[-1].max
583 self.axiscanvas = None
585 def getrange(self):
586 min = self.subaxes[0].getrange()
587 max = self.subaxes[-1].getrange()
588 try:
589 return min[0], max[1]
590 except TypeError:
591 return None
593 def setrange(self, min, max):
594 self.subaxes[0].setrange(min, None)
595 self.subaxes[-1].setrange(None, max)
597 def adjustrange(self, *args, **kwargs):
598 self.subaxes[0].adjustrange(*args, **kwargs)
599 self.subaxes[-1].adjustrange(*args, **kwargs)
601 def convert(self, value):
602 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
603 if value < self.subaxes[0].max:
604 return self.subaxes[0].vmin + self.subaxes[0].convert(value)*(self.subaxes[0].vmax-self.subaxes[0].vmin)
605 for axis in self.subaxes[1:-1]:
606 if value > axis.min and value < axis.max:
607 return axis.vmin + axis.convert(value)*(axis.vmax-axis.vmin)
608 if value > self.subaxes[-1].min:
609 return self.subaxes[-1].vmin + self.subaxes[-1].convert(value)*(self.subaxes[-1].vmax-self.subaxes[-1].vmin)
610 raise ValueError("value couldn't be assigned to a split region")
612 def finish(self, axispos):
613 if self.axiscanvas is None:
614 self.axiscanvas = self.painter.paint(axispos, self)
616 def createlinkaxis(self, **args):
617 return linkedsplit(self, **args)
620 class omitsubaxispainter: pass
622 class linkedsplit(_linked):
623 """a split axis linked to an already existing split axis
624 - inherits the access to a linked axis -- as before,
625 basically only the painter is replaced
626 - it takes care of the creation of linked axes of
627 the subaxes"""
629 __implements__ = _Iaxis
631 def __init__(self, linkedaxis, painter=painter.linkedsplit(), subaxispainter=omitsubaxispainter):
632 """initializes the instance
633 - linkedaxis is the axis this axis becomes linked to
634 - painter is axispainter instance for this linked axis
635 - subaxispainter is a changeable painter to be used for linked
636 subaxes; if omitsubaxispainter the createlinkaxis method of
637 the subaxis are called without a painter parameter"""
638 _linked.__init__(self, linkedaxis, painter=painter)
639 self.subaxes = []
640 for subaxis in linkedaxis.subaxes:
641 painter = attr.selectattr(subaxispainter, len(self.subaxes), len(linkedaxis.subaxes))
642 if painter is omitsubaxispainter:
643 self.subaxes.append(subaxis.createlinkaxis())
644 else:
645 self.subaxes.append(subaxis.createlinkaxis(painter=painter))
648 class bar:
649 """implementation of a axis for bar graphs
650 - a bar axes is different from a split axis by the way it
651 selects its subaxes: the convert method gets a list,
652 where the first entry is a name selecting a subaxis out
653 of a list; instead of the term "bar" or "subaxis" the term
654 "item" will be used here
655 - the bar axis stores a list of names be identify the items;
656 the names might be of any time (strings, integers, etc.);
657 the names can be printed as the titles for the items, but
658 alternatively the names might be transformed by the texts
659 dictionary, which maps a name to a text to be used to label
660 the items in the painter
661 - usually, there is only one subaxis, which is used as
662 the subaxis for all items
663 - alternatively it is also possible to use another bar axis
664 as a multisubaxis; it is copied via the createsubaxis
665 method whenever another subaxis is needed (by that a
666 nested bar axis with a different number of subbars at
667 each item can be created)
668 - any axis can be a subaxis of a bar axis; if no subaxis
669 is specified at all, the bar axis simulates a linear
670 subaxis with a fixed range of 0 to 1"""
672 def __init__(self, subaxis=None, multisubaxis=None,
673 dist=0.5, firstdist=None, lastdist=None,
674 names=None, texts={},
675 title=None, painter=painter.bar()):
676 """initialize the instance
677 - subaxis contains a axis to be used as the subaxis
678 for all items
679 - multisubaxis might contain another bar axis instance
680 to be used to construct a new subaxis for each item;
681 (by that a nested bar axis with a different number
682 of subbars at each item can be created)
683 - only one of subaxis or multisubaxis can be set; if neither
684 of them is set, the bar axis behaves like having a linear axis
685 as its subaxis with a fixed range 0 to 1
686 - the title attribute contains the axis title as a string
687 - the dist is a relsize to be used as the distance between
688 the items
689 - the firstdist and lastdist are the distance before the
690 first and after the last item, respectively; when set
691 to None (the default), 0.5*dist is used
692 - the relsize of the bar axis is the sum of the
693 relsizes including all distances between the items"""
694 self.dist = dist
695 if firstdist is not None:
696 self.firstdist = firstdist
697 else:
698 self.firstdist = 0.5 * dist
699 if lastdist is not None:
700 self.lastdist = lastdist
701 else:
702 self.lastdist = 0.5 * dist
703 self.relsizes = None
704 self.multisubaxis = multisubaxis
705 if self.multisubaxis is not None:
706 if subaxis is not None:
707 raise RuntimeError("either use subaxis or multisubaxis")
708 self.subaxis = []
709 else:
710 self.subaxis = subaxis
711 self.title = title
712 self.painter = painter
713 self.axiscanvas = None
714 self.names = []
716 def createsubaxis(self):
717 return bar(subaxis=self.multisubaxis.subaxis,
718 multisubaxis=self.multisubaxis.multisubaxis,
719 title=self.multisubaxis.title,
720 dist=self.multisubaxis.dist,
721 firstdist=self.multisubaxis.firstdist,
722 lastdist=self.multisubaxis.lastdist,
723 painter=self.multisubaxis.painter)
725 def getrange(self):
726 # TODO: we do not yet have a proper range handling for a bar axis
727 return None
729 def setrange(self, min=None, max=None):
730 # TODO: we do not yet have a proper range handling for a bar axis
731 raise RuntimeError("range handling for a bar axis is not implemented")
733 def setname(self, name, *subnames):
734 """add a name to identify an item at the bar axis
735 - by using subnames, nested name definitions are
736 possible
737 - a style (or the user itself) might use this to
738 insert new items into a bar axis
739 - setting self.relsizes to None forces later recalculation"""
740 if name not in self.names:
741 self.relsizes = None
742 self.names.append(name)
743 if self.multisubaxis is not None:
744 self.subaxis.append(self.createsubaxis())
745 if len(subnames):
746 if self.multisubaxis is not None:
747 if self.subaxis[self.names.index(name)].setname(*subnames):
748 self.relsizes = None
749 else:
750 if self.subaxis.setname(*subnames):
751 self.relsizes = None
752 return self.relsizes is not None
754 def adjustrange(self, points, index, subnames=[]):
755 for point in points:
756 if index is not None:
757 self.setname(point[index], *subnames)
758 else:
759 self.setname(point, *subnames)
761 def updaterelsizes(self):
762 # guess what it does: it recalculates relsize attribute
763 self.relsizes = [i*self.dist + self.firstdist for i in range(len(self.names) + 1)]
764 self.relsizes[-1] += self.lastdist - self.dist
765 if self.multisubaxis is not None:
766 subrelsize = 0
767 for i in range(1, len(self.relsizes)):
768 self.subaxis[i-1].updaterelsizes()
769 subrelsize += self.subaxis[i-1].relsizes[-1]
770 self.relsizes[i] += subrelsize
771 else:
772 if self.subaxis is None:
773 subrelsize = 1
774 else:
775 self.subaxis.updaterelsizes()
776 subrelsize = self.subaxis.relsizes[-1]
777 for i in range(1, len(self.relsizes)):
778 self.relsizes[i] += i * subrelsize
780 def convert(self, value):
781 """bar axis convert method
782 - the value should be a list, where the first entry is
783 a member of the names (set in the constructor or by the
784 setname method); this first entry identifies an item in
785 the bar axis
786 - following values are passed to the appropriate subaxis
787 convert method
788 - when there is no subaxis, the convert method will behave
789 like having a linear axis from 0 to 1 as subaxis"""
790 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
791 if not self.relsizes:
792 self.updaterelsizes()
793 pos = self.names.index(value[0])
794 if len(value) == 2:
795 if self.subaxis is None:
796 subvalue = value[1]
797 else:
798 if self.multisubaxis is not None:
799 subvalue = value[1] * self.subaxis[pos].relsizes[-1]
800 else:
801 subvalue = value[1] * self.subaxis.relsizes[-1]
802 else:
803 if self.multisubaxis is not None:
804 subvalue = self.subaxis[pos].convert(value[1:]) * self.subaxis[pos].relsizes[-1]
805 else:
806 subvalue = self.subaxis.convert(value[1:]) * self.subaxis.relsizes[-1]
807 return (self.relsizes[pos] + subvalue) / float(self.relsizes[-1])
809 def finish(self, axispos):
810 if self.axiscanvas is None:
811 if self.multisubaxis is not None:
812 for name, subaxis in zip(self.names, self.subaxis):
813 subaxis.vmin = self.convert((name, 0))
814 subaxis.vmax = self.convert((name, 1))
815 self.axiscanvas = self.painter.paint(axispos, self)
817 def createlinkaxis(self, **args):
818 return linkedbar(self, **args)
821 class linkedbar(_linked):
822 """a bar axis linked to an already existing bar axis
823 - inherits the access to a linked axis -- as before,
824 basically only the painter is replaced
825 - it must take care of the creation of linked axes of
826 the subaxes"""
828 __implements__ = _Iaxis
830 def __init__(self, linkedaxis, painter=painter.linkedbar()):
831 """initializes the instance
832 - it gets a axis this linkaxis is linked to
833 - it gets a painter to be used for this linked axis"""
834 _linked.__init__(self, linkedaxis, painter=painter)
835 if self.multisubaxis is not None:
836 self.subaxis = [subaxis.createlinkaxis() for subaxis in self.linkedaxis.subaxis]
837 elif self.subaxis is not None:
838 self.subaxis = self.subaxis.createlinkaxis()
841 def pathaxis(path, axis, **kwargs):
842 """creates an axiscanvas for an axis along a path"""
843 axis.finish(painter.pathaxispos(path, axis.convert, **kwargs))
844 return axis.axiscanvas