- some bargraph examples
[PyX/mjg.git] / pyx / graph / axis / axis.py
blobdc95ffd77d05e3437202df2a0d0ea0d4b85aed32
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 combined
178 - note that some methods of this class want to access a
179 parter and a rater; those attributes implementing _Iparter
180 and _Irater should be initialized by the constructors
181 of derived classes"""
182 if min is not None and max is not None and min > max:
183 min, max, reverse = max, min, not reverse
184 self.fixmin, self.fixmax, self.min, self.max, self.reverse = min is not None, max is not None, min, max, reverse
185 self.divisor = divisor
186 self.usedivisor = 0
187 self.title = title
188 self.painter = painter
189 self.texter = texter
190 self.density = density
191 self.maxworse = maxworse
192 self.manualticks = self.checkfraclist(manualticks)
193 self.axiscanvas = None
194 self._setrange()
196 def _setrange(self, min=None, max=None):
197 if not self.fixmin and min is not None and (self.min is None or min < self.min):
198 self.min = min
199 if not self.fixmax and max is not None and (self.max is None or max > self.max):
200 self.max = max
201 if None not in (self.min, self.max) and self.min != self.max:
202 if self.reverse:
203 if self.usedivisor and self.divisor is not None:
204 self.setbasepoints(((self.min/self.divisor, 1), (self.max/self.divisor, 0)))
205 else:
206 self.setbasepoints(((self.min, 1), (self.max, 0)))
207 else:
208 if self.usedivisor and self.divisor is not None:
209 self.setbasepoints(((self.min/self.divisor, 0), (self.max/self.divisor, 1)))
210 else:
211 self.setbasepoints(((self.min, 0), (self.max, 1)))
213 def _getrange(self):
214 return self.min, self.max
216 def _forcerange(self, range):
217 self.min, self.max = range
218 self._setrange()
220 def setrange(self, min=None, max=None):
221 if self.usedivisor and self.divisor is not None:
222 min = float(self.divisor) * self.divisor
223 max = float(self.divisor) * self.divisor
224 oldmin, oldmax = self.min, self.max
225 self._setrange(min, max)
226 if self.axiscanvas is not None and ((oldmin != self.min) or (oldmax != self.max)):
227 raise RuntimeError("range modification while axis was already finished")
229 zero = 0.0
231 def adjustrange(self, data, index, deltamindata=None, deltaminindex=None, deltamaxdata=None, deltamaxindex=None):
232 assert deltamindata is None or deltamaxdata is None
233 min = max = None
234 if deltamindata is not None:
235 for point, minpoint in zip(data, deltamindata):
236 try:
237 if index is not None:
238 if deltaminindex is not None:
239 value = point[index] - minpoint[deltaminindex] + self.zero
240 else:
241 value = point[index] - minpoint + self.zero
242 else:
243 if deltaminindex is not None:
244 value = point - minpoint[deltaminindex] + self.zero
245 else:
246 value = point - minpoint + self.zero
247 except:
248 pass
249 else:
250 if min is None or value < min: min = value
251 if max is None or value > max: max = value
252 elif deltamaxdata is not None:
253 for point, maxpoint in zip(data, deltamaxdata):
254 try:
255 if index is not None:
256 if deltamaxindex is not None:
257 value = point[index] + maxpoint[deltamaxindex] + self.zero
258 else:
259 value = point[index] + maxpoint + self.zero
260 else:
261 if deltamaxindex is not None:
262 value = point + maxpoint[deltamaxindex] + self.zero
263 else:
264 value = point + maxpoint + self.zero
265 except:
266 pass
267 else:
268 if min is None or value < min: min = value
269 if max is None or value > max: max = value
270 else:
271 for point in data:
272 try:
273 if index is not None:
274 value = point[index] + self.zero
275 else:
276 value = point + self.zero
277 except:
278 pass
279 else:
280 if min is None or value < min: min = value
281 if max is None or value > max: max = value
282 self.setrange(min, max)
284 def getrange(self):
285 if self.min is not None and self.max is not None:
286 if self.usedivisor and self.divisor is not None:
287 return float(self.min) / self.divisor, float(self.max) / self.divisor
288 else:
289 return self.min, self.max
291 def checkfraclist(self, fracs):
292 "orders a list of fracs, equal entries are not allowed"
293 if not len(fracs): return []
294 sorted = list(fracs)
295 sorted.sort()
296 last = sorted[0]
297 for item in sorted[1:]:
298 if last == item:
299 raise ValueError("duplicate entry found")
300 last = item
301 return sorted
303 def finish(self, axispos):
304 if self.axiscanvas is not None: return
306 # temorarily enable the axis divisor
307 self.usedivisor = 1
308 self._setrange()
310 # lesspart and morepart can be called after defaultpart;
311 # this works although some axes may share their autoparting,
312 # because the axes are processed sequentially
313 first = 1
314 if self.parter is not None:
315 min, max = self.getrange()
316 self.ticks = tick.mergeticklists(self.manualticks,
317 self.parter.defaultpart(min, max, not self.fixmin, not self.fixmax),
318 keepfirstifequal=1)
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, keepfirstifequal=1)
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 nodistances = 1
357 i = 0
358 bestrate = None
359 while i < len(variants) and (bestrate is None or variants[i][0] < bestrate):
360 saverange = self._getrange()
361 self.ticks = variants[i][1]
362 if len(self.ticks):
363 self.setrange(self.ticks[0], self.ticks[-1])
364 self.texter.labels(self.ticks)
365 ac = self.painter.paint(axispos, self)
366 if len(ac.labels) > 1:
367 nodistances = 0
368 ratelayout = self.rater.ratelayout(ac, self.density)
369 if ratelayout is not None:
370 variants[i][0] += ratelayout
371 else:
372 variants[i][0] = None
373 variants[i].append(ac)
374 if variants[i][0] is not None and (bestrate is None or variants[i][0] < bestrate):
375 bestrate = variants[i][0]
376 self._forcerange(saverange)
377 i += 1
378 if not nodistances:
379 if bestrate is None:
380 raise RuntimeError("no valid axis partitioning found")
381 variants = [variant for variant in variants[:i] if variant[0] is not None]
382 variants.sort()
383 self.ticks = variants[0][1]
384 if len(self.ticks):
385 self.setrange(self.ticks[0], self.ticks[-1])
386 self.axiscanvas = variants[0][2]
387 else:
388 self.ticks = variants[0][1]
389 self.texter.labels(self.ticks)
390 if len(self.ticks):
391 self.setrange(self.ticks[0], self.ticks[-1])
392 self.axiscanvas = painter.axiscanvas()
393 else:
394 if len(self.ticks):
395 self.setrange(self.ticks[0], self.ticks[-1])
396 self.texter.labels(self.ticks)
397 if self.painter is not None:
398 self.axiscanvas = self.painter.paint(axispos, self)
399 else:
400 self.axiscanvas = painter.axiscanvas()
402 # disable the axis divisor
403 self.usedivisor = 0
404 self._setrange()
406 def createlinkaxis(self, **args):
407 return linked(self, **args)
410 class linear(_axis, _linmap):
411 """implementation of a linear axis"""
413 __implements__ = _Iaxis
415 def __init__(self, parter=parter.autolinear(), rater=rater.linear(), **args):
416 """initializes the instance
417 - the parter attribute implements _Iparter
418 - the rater implements _Irater and is used to rate different
419 tick lists created by the partitioner (after merging with
420 manully set ticks)
421 - futher keyword arguments are passed to _axis"""
422 _axis.__init__(self, **args)
423 if self.fixmin and self.fixmax:
424 self.relsize = self.max - self.min
425 self.parter = parter
426 self.rater = rater
428 lin = linear
431 class logarithmic(_axis, _logmap):
432 """implementation of a logarithmic axis"""
434 __implements__ = _Iaxis
436 def __init__(self, parter=parter.autologarithmic(), rater=rater.logarithmic(), **args):
437 """initializes the instance
438 - the parter attribute implements _Iparter
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 - usually, there is only one subaxis, which is used as
658 the subaxis for all items
659 - alternatively it is also possible to use another bar axis
660 as a multisubaxis; it is copied via the createsubaxis
661 method whenever another subaxis is needed (by that a
662 nested bar axis with a different number of subbars at
663 each item can be created)
664 - any axis can be a subaxis of a bar axis; if no subaxis
665 is specified at all, the bar axis simulates a linear
666 subaxis with a fixed range of 0 to 1"""
668 def __init__(self, subaxis=None, multisubaxis=None,
669 dist=0.5, firstdist=None, lastdist=None,
670 title=None, painter=painter.bar()):
671 """initialize the instance
672 - subaxis contains a axis to be used as the subaxis
673 for all items
674 - multisubaxis might contain another bar axis instance
675 to be used to construct a new subaxis for each item;
676 (by that a nested bar axis with a different number
677 of subbars at each item can be created)
678 - only one of subaxis or multisubaxis can be set; if neither
679 of them is set, the bar axis behaves like having a linear axis
680 as its subaxis with a fixed range 0 to 1
681 - the title attribute contains the axis title as a string
682 - the dist is a relsize to be used as the distance between
683 the items
684 - the firstdist and lastdist are the distance before the
685 first and after the last item, respectively; when set
686 to None (the default), 0.5*dist is used
687 - the relsize of the bar axis is the sum of the
688 relsizes including all distances between the items"""
689 self.dist = dist
690 if firstdist is not None:
691 self.firstdist = firstdist
692 else:
693 self.firstdist = 0.5 * dist
694 if lastdist is not None:
695 self.lastdist = lastdist
696 else:
697 self.lastdist = 0.5 * dist
698 self.relsizes = None
699 self.multisubaxis = multisubaxis
700 if self.multisubaxis is not None:
701 if subaxis is not None:
702 raise RuntimeError("either use subaxis or multisubaxis")
703 self.subaxis = []
704 else:
705 self.subaxis = subaxis
706 self.title = title
707 self.painter = painter
708 self.axiscanvas = None
709 self.names = []
711 def createsubaxis(self):
712 return bar(subaxis=self.multisubaxis.subaxis,
713 multisubaxis=self.multisubaxis.multisubaxis,
714 title=self.multisubaxis.title,
715 dist=self.multisubaxis.dist,
716 firstdist=self.multisubaxis.firstdist,
717 lastdist=self.multisubaxis.lastdist,
718 painter=self.multisubaxis.painter)
720 def getrange(self):
721 # TODO: we do not yet have a proper range handling for a bar axis
722 return None
724 def setrange(self, min=None, max=None):
725 # TODO: we do not yet have a proper range handling for a bar axis
726 raise RuntimeError("range handling for a bar axis is not implemented")
728 def setname(self, name, *subnames):
729 """add a name to identify an item at the bar axis
730 - by using subnames, nested name definitions are
731 possible
732 - a style (or the user itself) might use this to
733 insert new items into a bar axis
734 - setting self.relsizes to None forces later recalculation"""
735 if name not in self.names:
736 self.relsizes = None
737 self.names.append(name)
738 if self.multisubaxis is not None:
739 self.subaxis.append(self.createsubaxis())
740 if len(subnames):
741 if self.multisubaxis is not None:
742 if self.subaxis[self.names.index(name)].setname(*subnames):
743 self.relsizes = None
744 else:
745 if self.subaxis.setname(*subnames):
746 self.relsizes = None
747 return self.relsizes is not None
749 def adjustrange(self, points, index, subnames=[]):
750 for point in points:
751 if index is not None:
752 self.setname(point[index], *subnames)
753 else:
754 self.setname(point, *subnames)
756 def updaterelsizes(self):
757 # guess what it does: it recalculates relsize attribute
758 self.relsizes = [i*self.dist + self.firstdist for i in range(len(self.names) + 1)]
759 self.relsizes[-1] += self.lastdist - self.dist
760 if self.multisubaxis is not None:
761 subrelsize = 0
762 for i in range(1, len(self.relsizes)):
763 self.subaxis[i-1].updaterelsizes()
764 subrelsize += self.subaxis[i-1].relsizes[-1]
765 self.relsizes[i] += subrelsize
766 else:
767 if self.subaxis is None:
768 subrelsize = 1
769 else:
770 self.subaxis.updaterelsizes()
771 subrelsize = self.subaxis.relsizes[-1]
772 for i in range(1, len(self.relsizes)):
773 self.relsizes[i] += i * subrelsize
775 def convert(self, value):
776 """bar axis convert method
777 - the value should be a list, where the first entry is
778 a member of the names (set in the constructor or by the
779 setname method); this first entry identifies an item in
780 the bar axis
781 - following values are passed to the appropriate subaxis
782 convert method
783 - when there is no subaxis, the convert method will behave
784 like having a linear axis from 0 to 1 as subaxis"""
785 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
786 if not self.relsizes:
787 self.updaterelsizes()
788 pos = self.names.index(value[0])
789 if len(value) == 2:
790 if self.subaxis is None:
791 subvalue = value[1]
792 else:
793 if self.multisubaxis is not None:
794 subvalue = value[1] * self.subaxis[pos].relsizes[-1]
795 else:
796 subvalue = value[1] * self.subaxis.relsizes[-1]
797 else:
798 if self.multisubaxis is not None:
799 subvalue = self.subaxis[pos].convert(value[1:]) * self.subaxis[pos].relsizes[-1]
800 else:
801 subvalue = self.subaxis.convert(value[1:]) * self.subaxis.relsizes[-1]
802 return (self.relsizes[pos] + subvalue) / float(self.relsizes[-1])
804 def finish(self, axispos):
805 if self.axiscanvas is None:
806 if self.multisubaxis is not None:
807 for name, subaxis in zip(self.names, self.subaxis):
808 subaxis.vmin = self.convert((name, 0))
809 subaxis.vmax = self.convert((name, 1))
810 self.axiscanvas = self.painter.paint(axispos, self)
812 def createlinkaxis(self, **args):
813 return linkedbar(self, **args)
816 class linkedbar(_linked):
817 """a bar axis linked to an already existing bar axis
818 - inherits the access to a linked axis -- as before,
819 basically only the painter is replaced
820 - it must take care of the creation of linked axes of
821 the subaxes"""
823 __implements__ = _Iaxis
825 def __init__(self, linkedaxis, painter=painter.linkedbar()):
826 """initializes the instance
827 - it gets a axis this linkaxis is linked to
828 - it gets a painter to be used for this linked axis"""
829 _linked.__init__(self, linkedaxis, painter=painter)
830 if self.multisubaxis is not None:
831 self.subaxis = [subaxis.createlinkaxis() for subaxis in self.linkedaxis.subaxis]
832 elif self.subaxis is not None:
833 self.subaxis = self.subaxis.createlinkaxis()
836 def pathaxis(path, axis, **kwargs):
837 """creates an axiscanvas for an axis along a path"""
838 axis.finish(painter.pathaxispos(path, axis.convert, **kwargs))
839 return axis.axiscanvas