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
27 from pyx
import attr
, helper
28 from pyx
.graph
.axis
import painter
, parter
, rater
, texter
, tick
32 """interface definition of a map
33 maps convert a value into another value by bijective transformation f"""
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."""
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
)
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
))
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
95 - an axis might have further instance variables (title, ticks)
96 to be used in combination with appropriate axispainters"""
99 "convert a value into graph coordinates"
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
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"""
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
):
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"""
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
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
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
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
189 self
.painter
= painter
191 self
.density
= density
192 self
.maxworse
= maxworse
193 self
.manualticks
= self
.checkfraclist(manualticks
)
194 self
.axiscanvas
= None
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):
200 if not self
.fixmax
and max is not None and (self
.max is None or max > self
.max):
202 if None not in (self
.min, self
.max) and self
.min != self
.max:
204 if self
.usedivisor
and self
.divisor
is not None:
205 self
.setbasepoints(((self
.min/self
.divisor
, 1), (self
.max/self
.divisor
, 0)))
207 self
.setbasepoints(((self
.min, 1), (self
.max, 0)))
209 if self
.usedivisor
and self
.divisor
is not None:
210 self
.setbasepoints(((self
.min/self
.divisor
, 0), (self
.max/self
.divisor
, 1)))
212 self
.setbasepoints(((self
.min, 0), (self
.max, 1)))
215 return self
.min, self
.max
217 def _forcerange(self
, range):
218 self
.min, self
.max = range
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")
232 def adjustrange(self
, points
, index
, deltaindex
=None, deltaminindex
=None, deltamaxindex
=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:
241 value
= point
[index
] - point
[deltaminindex
] + self
.zero
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:
250 value
= point
[index
] + point
[deltamaxindex
] + self
.zero
254 if min is None or value
< min: min = value
255 if max is None or value
> max: max = value
259 value
= point
[index
] + self
.zero
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)
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
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 []
280 for item
in sorted[1:]:
282 raise ValueError("duplicate entry found")
286 def finish(self
, axispos
):
287 if self
.axiscanvas
is not None: return
289 # temorarily enable the axis divisor
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
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
))
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
)
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
]]
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
:
324 if worse
== self
.maxworse
and nextpart
== self
.parter
.lesspart
:
326 nextpart
= self
.parter
.morepart
327 if worse
== self
.maxworse
and nextpart
== self
.parter
.morepart
:
330 self
.ticks
=self
.manualticks
332 # rating, when several choises are available
335 if self
.painter
is not 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]
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
)
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
)
356 raise RuntimeError("no valid axis partitioning found")
357 variants
= [variant
for variant
in variants
[:i
] if variant
[0] is not None]
359 self
.ticks
= variants
[0][1]
361 self
.setrange(self
.ticks
[0], self
.ticks
[-1])
362 self
.axiscanvas
= variants
[0][2]
364 self
.ticks
= variants
[0][1]
365 self
.texter
.labels(self
.ticks
)
367 self
.setrange(self
.ticks
[0], self
.ticks
[-1])
368 self
.axiscanvas
= painter
.axiscanvas()
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
)
376 self
.axiscanvas
= painter
.axiscanvas()
378 # disable the axis divisor
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
396 - the rater implements _Irater and is used to rate different
397 tick lists created by the partitioner (after merging with
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
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
419 - the rater implements _Irater and is used to rate different
420 tick lists created by the partitioner (after merging with
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)
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
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
):
459 - instead of performing the hole finish process
460 (paritioning, rating, etc.) just a painter call
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
()
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
511 - title is the title of the axis as a string
512 - painter is the painter of the axis; it should be specialized to
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
519 self
.splitlist
= splitlist
520 for subaxis
in self
.subaxes
:
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
]
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
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
556 self
.fixmin
= self
.subaxes
[0].fixmin
558 self
.min = self
.subaxes
[0].min
559 self
.fixmax
= self
.subaxes
[-1].fixmax
561 self
.max = self
.subaxes
[-1].max
563 self
.axiscanvas
= None
566 min = self
.subaxes
[0].getrange()
567 max = self
.subaxes
[-1].getrange()
569 return min[0], max[1]
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
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
)
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())
625 self
.subaxes
.append(subaxis
.createlinkaxis(painter
=painter
))
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
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
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"""
675 if firstdist
is not None:
676 self
.firstdist
= firstdist
678 self
.firstdist
= 0.5 * dist
679 if lastdist
is not None:
680 self
.lastdist
= lastdist
682 self
.lastdist
= 0.5 * dist
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")
690 self
.subaxis
= subaxis
692 self
.painter
= painter
693 self
.axiscanvas
= None
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
)
706 # TODO: we do not yet have a proper range handling for a bar axis
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
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
:
722 self
.names
.append(name
)
723 if self
.multisubaxis
is not None:
724 self
.subaxis
.append(self
.createsubaxis())
726 if self
.multisubaxis
is not None:
727 if self
.subaxis
[self
.names
.index(name
)].setname(*subnames
):
730 if self
.subaxis
.setname(*subnames
):
732 return self
.relsizes
is not None
734 def adjustrange(self
, points
, index
, subnames
=None):
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:
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
751 if self
.subaxis
is None:
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
765 - following values are passed to the appropriate subaxis
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])
774 if self
.subaxis
is None:
777 if self
.multisubaxis
is not None:
778 subvalue
= value
[1] * self
.subaxis
[pos
].relsizes
[-1]
780 subvalue
= value
[1] * self
.subaxis
.relsizes
[-1]
782 if self
.multisubaxis
is not None:
783 subvalue
= self
.subaxis
[pos
].convert(value
[1:]) * self
.subaxis
[pos
].relsizes
[-1]
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
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