code cleanup
[PyX/mjg.git] / pyx / graph / axis / axis.py
blob5bd2dccf8e3f34e045b1559736b144391867d198
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, points, index, deltaindex=None, deltaminindex=None, deltamaxindex=None):
233 min = max = None
234 if len([x for x in [deltaindex, deltaminindex, deltamaxindex] if x is not None]) > 1:
235 raise RuntimeError("only one of delta???index should set")
236 if deltaindex is not None:
237 deltaminindex = deltamaxindex = deltaindex
238 if deltaminindex is not None:
239 for point in points:
240 try:
241 value = point[index] - point[deltaminindex] + self.zero
242 except:
243 pass
244 else:
245 if min is None or value < min: min = value
246 if max is None or value > max: max = value
247 if deltamaxindex is not None:
248 for point in points:
249 try:
250 value = point[index] + point[deltamaxindex] + self.zero
251 except:
252 pass
253 else:
254 if min is None or value < min: min = value
255 if max is None or value > max: max = value
256 for point in points:
257 try:
258 value = point[index] + self.zero
259 except:
260 pass
261 else:
262 if min is None or value < min: min = value
263 if max is None or value > max: max = value
264 self.setrange(min, max)
266 def getrange(self):
267 if self.min is not None and self.max is not None:
268 if self.usedivisor and self.divisor is not None:
269 return float(self.min) / self.divisor, float(self.max) / self.divisor
270 else:
271 return self.min, self.max
273 def checkfraclist(self, fracs):
274 "orders a list of fracs, equal entries are not allowed"
275 if not len(fracs): return []
276 sorted = list(fracs)
277 sorted.sort()
278 last = sorted[0]
279 for item in sorted[1:]:
280 if last == item:
281 raise ValueError("duplicate entry found")
282 last = item
283 return sorted
285 def finish(self, axispos):
286 if self.axiscanvas is not None: return
288 # temorarily enable the axis divisor
289 self.usedivisor = 1
290 self._setrange()
292 # lesspart and morepart can be called after defaultpart;
293 # this works although some axes may share their autoparting,
294 # because the axes are processed sequentially
295 first = 1
296 if self.parter is not None:
297 min, max = self.getrange()
298 self.ticks = tick.mergeticklists(self.manualticks,
299 self.parter.defaultpart(min, max, not self.fixmin, not self.fixmax))
300 worse = 0
301 nextpart = self.parter.lesspart
302 while nextpart is not None:
303 newticks = nextpart()
304 worse += 1
305 if newticks is not None:
306 newticks = tick.mergeticklists(self.manualticks, newticks)
307 if first:
308 if len(self.ticks):
309 bestrate = self.rater.rateticks(self, self.ticks, self.density)
310 bestrate += self.rater.raterange(self.convert(self.ticks[-1])-
311 self.convert(self.ticks[0]), 1)
312 variants = [[bestrate, self.ticks]]
313 else:
314 bestrate = None
315 variants = []
316 first = 0
317 if len(newticks):
318 newrate = self.rater.rateticks(self, newticks, self.density)
319 newrate += self.rater.raterange(self.convert(newticks[-1])-
320 self.convert(newticks[0]), 1)
321 variants.append([newrate, newticks])
322 if bestrate is None or newrate < bestrate:
323 bestrate = newrate
324 worse = 0
325 if worse == self.maxworse and nextpart == self.parter.lesspart:
326 worse = 0
327 nextpart = self.parter.morepart
328 if worse == self.maxworse and nextpart == self.parter.morepart:
329 nextpart = None
330 else:
331 self.ticks =self.manualticks
333 # rating, when several choises are available
334 if not first:
335 variants.sort()
336 if self.painter is not None:
337 i = 0
338 bestrate = None
339 while i < len(variants) and (bestrate is None or variants[i][0] < bestrate):
340 saverange = self._getrange()
341 self.ticks = variants[i][1]
342 if len(self.ticks):
343 self.setrange(self.ticks[0], self.ticks[-1])
344 self.texter.labels(self.ticks)
345 ac = self.painter.paint(axispos, self)
346 ratelayout = self.rater.ratelayout(ac, self.density)
347 if ratelayout is not None:
348 variants[i][0] += ratelayout
349 variants[i].append(ac)
350 else:
351 variants[i][0] = None
352 if variants[i][0] is not None and (bestrate is None or variants[i][0] < bestrate):
353 bestrate = variants[i][0]
354 self._forcerange(saverange)
355 i += 1
356 if bestrate is None:
357 raise RuntimeError("no valid axis partitioning found")
358 variants = [variant for variant in variants[:i] if variant[0] is not None]
359 variants.sort()
360 self.ticks = variants[0][1]
361 if len(self.ticks):
362 self.setrange(self.ticks[0], self.ticks[-1])
363 self.axiscanvas = variants[0][2]
364 else:
365 self.ticks = variants[0][1]
366 self.texter.labels(self.ticks)
367 if len(self.ticks):
368 self.setrange(self.ticks[0], self.ticks[-1])
369 self.axiscanvas = painter.axiscanvas()
370 else:
371 if len(self.ticks):
372 self.setrange(self.ticks[0], self.ticks[-1])
373 self.texter.labels(self.ticks)
374 if self.painter is not None:
375 self.axiscanvas = self.painter.paint(axispos, self)
376 else:
377 self.axiscanvas = painter.axiscanvas()
379 # disable the axis divisor
380 self.usedivisor = 0
381 self._setrange()
383 def createlinkaxis(self, **args):
384 return linked(self, **args)
387 class linear(_axis, _linmap):
388 """implementation of a linear axis"""
390 __implements__ = _Iaxis
392 def __init__(self, parter=parter.autolinear(), rater=rater.linear(), **args):
393 """initializes the instance
394 - the parter attribute implements _Iparter
395 - manualticks and the partitioner results are mixed
396 by mergeticklists
397 - the rater implements _Irater and is used to rate different
398 tick lists created by the partitioner (after merging with
399 manully set ticks)
400 - futher keyword arguments are passed to _axis"""
401 _axis.__init__(self, **args)
402 if self.fixmin and self.fixmax:
403 self.relsize = self.max - self.min
404 self.parter = parter
405 self.rater = rater
407 lin = linear
410 class logarithmic(_axis, _logmap):
411 """implementation of a logarithmic axis"""
413 __implements__ = _Iaxis
415 def __init__(self, parter=parter.autologarithmic(), rater=rater.logarithmic(), **args):
416 """initializes the instance
417 - the parter attribute implements _Iparter
418 - manualticks and the partitioner results are mixed
419 by mergeticklists
420 - the rater implements _Irater and is used to rate different
421 tick lists created by the partitioner (after merging with
422 manully set ticks)
423 - futher keyword arguments are passed to _axis"""
424 _axis.__init__(self, **args)
425 if self.fixmin and self.fixmax:
426 self.relsize = math.log(self.max) - math.log(self.min)
427 self.parter = parter
428 self.rater = rater
430 log = logarithmic
433 class _linked:
434 """base for a axis linked to an already existing axis
435 - almost all properties of the axis are "copied" from the
436 axis this axis is linked to
437 - usually, linked axis are used to create an axis to an
438 existing axis with different painting properties; linked
439 axis can be used to plot an axis twice at the opposite
440 sides of a graphxy or even to share an axis between
441 different graphs!"""
443 __implements__ = _Iaxis
445 def __init__(self, linkedaxis, painter=painter.linked()):
446 """initializes the instance
447 - it gets a axis this axis is linked to
448 - it gets a painter to be used for this linked axis"""
449 self.linkedaxis = linkedaxis
450 self.painter = painter
451 self.axiscanvas = None
453 def __getattr__(self, attr):
454 """access to unkown attributes are handed over to the
455 axis this axis is linked to"""
456 return getattr(self.linkedaxis, attr)
458 def finish(self, axispos):
459 """finishes the axis
460 - instead of performing the hole finish process
461 (paritioning, rating, etc.) just a painter call
462 is performed"""
464 if self.axiscanvas is None:
465 if self.linkedaxis.axiscanvas is None:
466 raise RuntimeError("link axis finish method called before the finish method of the original axis")
467 self.axiscanvas = self.painter.paint(axispos, self)
470 class linked(_linked):
471 """a axis linked to an already existing regular axis
472 - adds divisor handling to _linked"""
474 __implements__ = _Iaxis
476 def finish(self, axispos):
477 # temporarily enable the linkedaxis divisor
478 self.linkedaxis.usedivisor = 1
479 self.linkedaxis._setrange()
481 _linked.finish(self, axispos)
483 # disable the linkedaxis divisor again
484 self.linkedaxis.usedivisor = 0
485 self.linkedaxis._setrange()
488 class split:
489 """implementation of a split axis
490 - a split axis contains several (sub-)axes with
491 non-overlapping data ranges -- between these subaxes
492 the axis is "splitted"
493 - (just to get sure: a split axis can contain other
494 split axes as its subaxes)"""
496 __implements__ = _Iaxis, painter._Iaxispos
498 def __init__(self, subaxes, splitlist=[0.5], splitdist=0.1, relsizesplitdist=1,
499 title=None, painter=painter.split()):
500 """initializes the instance
501 - subaxes is a list of subaxes
502 - splitlist is a list of graph coordinates, where the splitting
503 of the main axis should be performed; if the list isn't long enough
504 for the subaxes, missing entries are considered to be None
505 - splitdist is the size of the splitting in graph coordinates, when
506 the associated splitlist entry is not None
507 - relsizesplitdist: a None entry in splitlist means, that the
508 position of the splitting should be calculated out of the
509 relsize values of conrtibuting subaxes (the size of the
510 splitting is relsizesplitdist in values of the relsize values
511 of the axes)
512 - title is the title of the axis as a string
513 - painter is the painter of the axis; it should be specialized to
514 the split axis
515 - the relsize of the split axis is the sum of the relsizes of the
516 subaxes including the relsizesplitdist"""
517 self.subaxes = subaxes
518 self.painter = painter
519 self.title = title
520 self.splitlist = splitlist
521 for subaxis in self.subaxes:
522 subaxis.vmin = None
523 subaxis.vmax = None
524 self.subaxes[0].vmin = 0
525 self.subaxes[0].vminover = None
526 self.subaxes[-1].vmax = 1
527 self.subaxes[-1].vmaxover = None
528 for i in xrange(len(self.splitlist)):
529 if self.splitlist[i] is not None:
530 self.subaxes[i].vmax = self.splitlist[i] - 0.5*splitdist
531 self.subaxes[i].vmaxover = self.splitlist[i]
532 self.subaxes[i+1].vmin = self.splitlist[i] + 0.5*splitdist
533 self.subaxes[i+1].vminover = self.splitlist[i]
534 i = 0
535 while i < len(self.subaxes):
536 if self.subaxes[i].vmax is None:
537 j = relsize = relsize2 = 0
538 while self.subaxes[i + j].vmax is None:
539 relsize += self.subaxes[i + j].relsize + relsizesplitdist
540 j += 1
541 relsize += self.subaxes[i + j].relsize
542 vleft = self.subaxes[i].vmin
543 vright = self.subaxes[i + j].vmax
544 for k in range(i, i + j):
545 relsize2 += self.subaxes[k].relsize
546 self.subaxes[k].vmax = vleft + (vright - vleft) * relsize2 / float(relsize)
547 relsize2 += 0.5 * relsizesplitdist
548 self.subaxes[k].vmaxover = self.subaxes[k + 1].vminover = vleft + (vright - vleft) * relsize2 / float(relsize)
549 relsize2 += 0.5 * relsizesplitdist
550 self.subaxes[k+1].vmin = vleft + (vright - vleft) * relsize2 / float(relsize)
551 if i == 0 and i + j + 1 == len(self.subaxes):
552 self.relsize = relsize
553 i += j + 1
554 else:
555 i += 1
557 self.fixmin = self.subaxes[0].fixmin
558 if self.fixmin:
559 self.min = self.subaxes[0].min
560 self.fixmax = self.subaxes[-1].fixmax
561 if self.fixmax:
562 self.max = self.subaxes[-1].max
564 self.axiscanvas = None
566 def getrange(self):
567 min = self.subaxes[0].getrange()
568 max = self.subaxes[-1].getrange()
569 try:
570 return min[0], max[1]
571 except TypeError:
572 return None
574 def setrange(self, min, max):
575 self.subaxes[0].setrange(min, None)
576 self.subaxes[-1].setrange(None, max)
578 def adjustrange(self, *args, **kwargs):
579 self.subaxes[0].adjustrange(*args, **kwargs)
580 self.subaxes[-1].adjustrange(*args, **kwargs)
582 def convert(self, value):
583 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
584 if value < self.subaxes[0].max:
585 return self.subaxes[0].vmin + self.subaxes[0].convert(value)*(self.subaxes[0].vmax-self.subaxes[0].vmin)
586 for axis in self.subaxes[1:-1]:
587 if value > axis.min and value < axis.max:
588 return axis.vmin + axis.convert(value)*(axis.vmax-axis.vmin)
589 if value > self.subaxes[-1].min:
590 return self.subaxes[-1].vmin + self.subaxes[-1].convert(value)*(self.subaxes[-1].vmax-self.subaxes[-1].vmin)
591 raise ValueError("value couldn't be assigned to a split region")
593 def finish(self, axispos):
594 if self.axiscanvas is None:
595 self.axiscanvas = self.painter.paint(axispos, self)
597 def createlinkaxis(self, **args):
598 return linkedsplit(self, **args)
601 class omitsubaxispainter: pass
603 class linkedsplit(_linked):
604 """a split axis linked to an already existing split axis
605 - inherits the access to a linked axis -- as before,
606 basically only the painter is replaced
607 - it takes care of the creation of linked axes of
608 the subaxes"""
610 __implements__ = _Iaxis
612 def __init__(self, linkedaxis, painter=painter.linkedsplit(), subaxispainter=omitsubaxispainter):
613 """initializes the instance
614 - linkedaxis is the axis this axis becomes linked to
615 - painter is axispainter instance for this linked axis
616 - subaxispainter is a changeable painter to be used for linked
617 subaxes; if omitsubaxispainter the createlinkaxis method of
618 the subaxis are called without a painter parameter"""
619 _linked.__init__(self, linkedaxis, painter=painter)
620 self.subaxes = []
621 for subaxis in linkedaxis.subaxes:
622 painter = attr.selectattr(subaxispainter, len(self.subaxes), len(linkedaxis.subaxes))
623 if painter is omitsubaxispainter:
624 self.subaxes.append(subaxis.createlinkaxis())
625 else:
626 self.subaxes.append(subaxis.createlinkaxis(painter=painter))
629 class bar:
630 """implementation of a axis for bar graphs
631 - a bar axes is different from a split axis by the way it
632 selects its subaxes: the convert method gets a list,
633 where the first entry is a name selecting a subaxis out
634 of a list; instead of the term "bar" or "subaxis" the term
635 "item" will be used here
636 - the bar axis stores a list of names be identify the items;
637 the names might be of any time (strings, integers, etc.);
638 the names can be printed as the titles for the items, but
639 alternatively the names might be transformed by the texts
640 dictionary, which maps a name to a text to be used to label
641 the items in the painter
642 - usually, there is only one subaxis, which is used as
643 the subaxis for all items
644 - alternatively it is also possible to use another bar axis
645 as a multisubaxis; it is copied via the createsubaxis
646 method whenever another subaxis is needed (by that a
647 nested bar axis with a different number of subbars at
648 each item can be created)
649 - any axis can be a subaxis of a bar axis; if no subaxis
650 is specified at all, the bar axis simulates a linear
651 subaxis with a fixed range of 0 to 1"""
653 def __init__(self, subaxis=None, multisubaxis=None,
654 dist=0.5, firstdist=None, lastdist=None,
655 names=None, texts={},
656 title=None, painter=painter.bar()):
657 """initialize the instance
658 - subaxis contains a axis to be used as the subaxis
659 for all items
660 - multisubaxis might contain another bar axis instance
661 to be used to construct a new subaxis for each item;
662 (by that a nested bar axis with a different number
663 of subbars at each item can be created)
664 - only one of subaxis or multisubaxis can be set; if neither
665 of them is set, the bar axis behaves like having a linear axis
666 as its subaxis with a fixed range 0 to 1
667 - the title attribute contains the axis title as a string
668 - the dist is a relsize to be used as the distance between
669 the items
670 - the firstdist and lastdist are the distance before the
671 first and after the last item, respectively; when set
672 to None (the default), 0.5*dist is used
673 - the relsize of the bar axis is the sum of the
674 relsizes including all distances between the items"""
675 self.dist = dist
676 if firstdist is not None:
677 self.firstdist = firstdist
678 else:
679 self.firstdist = 0.5 * dist
680 if lastdist is not None:
681 self.lastdist = lastdist
682 else:
683 self.lastdist = 0.5 * dist
684 self.relsizes = None
685 self.multisubaxis = multisubaxis
686 if self.multisubaxis is not None:
687 if subaxis is not None:
688 raise RuntimeError("either use subaxis or multisubaxis")
689 self.subaxis = []
690 else:
691 self.subaxis = subaxis
692 self.title = title
693 self.painter = painter
694 self.axiscanvas = None
695 self.names = []
697 def createsubaxis(self):
698 return bar(subaxis=self.multisubaxis.subaxis,
699 multisubaxis=self.multisubaxis.multisubaxis,
700 title=self.multisubaxis.title,
701 dist=self.multisubaxis.dist,
702 firstdist=self.multisubaxis.firstdist,
703 lastdist=self.multisubaxis.lastdist,
704 painter=self.multisubaxis.painter)
706 def getrange(self):
707 # TODO: we do not yet have a proper range handling for a bar axis
708 return None
710 def setrange(self, min=None, max=None):
711 # TODO: we do not yet have a proper range handling for a bar axis
712 raise RuntimeError("range handling for a bar axis is not implemented")
714 def setname(self, name, *subnames):
715 """add a name to identify an item at the bar axis
716 - by using subnames, nested name definitions are
717 possible
718 - a style (or the user itself) might use this to
719 insert new items into a bar axis
720 - setting self.relsizes to None forces later recalculation"""
721 if name not in self.names:
722 self.relsizes = None
723 self.names.append(name)
724 if self.multisubaxis is not None:
725 self.subaxis.append(self.createsubaxis())
726 if len(subnames):
727 if self.multisubaxis is not None:
728 if self.subaxis[self.names.index(name)].setname(*subnames):
729 self.relsizes = None
730 else:
731 if self.subaxis.setname(*subnames):
732 self.relsizes = None
733 return self.relsizes is not None
735 def adjustrange(self, points, index, subnames=None):
736 if subnames is None:
737 subnames = []
738 for point in points:
739 self.setname(point[index], *subnames)
741 def updaterelsizes(self):
742 # guess what it does: it recalculates relsize attribute
743 self.relsizes = [i*self.dist + self.firstdist for i in range(len(self.names) + 1)]
744 self.relsizes[-1] += self.lastdist - self.dist
745 if self.multisubaxis is not None:
746 subrelsize = 0
747 for i in range(1, len(self.relsizes)):
748 self.subaxis[i-1].updaterelsizes()
749 subrelsize += self.subaxis[i-1].relsizes[-1]
750 self.relsizes[i] += subrelsize
751 else:
752 if self.subaxis is None:
753 subrelsize = 1
754 else:
755 self.subaxis.updaterelsizes()
756 subrelsize = self.subaxis.relsizes[-1]
757 for i in range(1, len(self.relsizes)):
758 self.relsizes[i] += i * subrelsize
760 def convert(self, value):
761 """bar axis convert method
762 - the value should be a list, where the first entry is
763 a member of the names (set in the constructor or by the
764 setname method); this first entry identifies an item in
765 the bar axis
766 - following values are passed to the appropriate subaxis
767 convert method
768 - when there is no subaxis, the convert method will behave
769 like having a linear axis from 0 to 1 as subaxis"""
770 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
771 if not self.relsizes:
772 self.updaterelsizes()
773 pos = self.names.index(value[0])
774 if len(value) == 2:
775 if self.subaxis is None:
776 subvalue = value[1]
777 else:
778 if self.multisubaxis is not None:
779 subvalue = value[1] * self.subaxis[pos].relsizes[-1]
780 else:
781 subvalue = value[1] * self.subaxis.relsizes[-1]
782 else:
783 if self.multisubaxis is not None:
784 subvalue = self.subaxis[pos].convert(value[1:]) * self.subaxis[pos].relsizes[-1]
785 else:
786 subvalue = self.subaxis.convert(value[1:]) * self.subaxis.relsizes[-1]
787 return (self.relsizes[pos] + subvalue) / float(self.relsizes[-1])
789 def finish(self, axispos):
790 if self.axiscanvas is None:
791 if self.multisubaxis is not None:
792 for name, subaxis in zip(self.names, self.subaxis):
793 subaxis.vmin = self.convert((name, 0))
794 subaxis.vmax = self.convert((name, 1))
795 self.axiscanvas = self.painter.paint(axispos, self)
797 def createlinkaxis(self, **args):
798 return linkedbar(self, **args)
801 class linkedbar(_linked):
802 """a bar axis linked to an already existing bar axis
803 - inherits the access to a linked axis -- as before,
804 basically only the painter is replaced
805 - it must take care of the creation of linked axes of
806 the subaxes"""
808 __implements__ = _Iaxis
810 def __init__(self, linkedaxis, painter=painter.linkedbar()):
811 """initializes the instance
812 - it gets a axis this linkaxis is linked to
813 - it gets a painter to be used for this linked axis"""
814 _linked.__init__(self, linkedaxis, painter=painter)
815 if self.multisubaxis is not None:
816 self.subaxis = [subaxis.createlinkaxis() for subaxis in self.linkedaxis.subaxis]
817 elif self.subaxis is not None:
818 self.subaxis = self.subaxis.createlinkaxis()
821 def pathaxis(path, axis, **kwargs):
822 """creates an axiscanvas for an axis along a path"""
823 axis.finish(painter.pathaxispos(path, axis.convert, **kwargs))
824 return axis.axiscanvas