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