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
.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
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 if 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
258 value
= point
[index
] + self
.zero
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)
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
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 []
279 for item
in sorted[1:]:
281 raise ValueError("duplicate entry found")
285 def finish(self
, axispos
):
286 if self
.axiscanvas
is not None: return
288 # temorarily enable the axis divisor
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
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
))
301 nextpart
= self
.parter
.lesspart
302 while nextpart
is not None:
303 newticks
= nextpart()
305 if newticks
is not None:
306 newticks
= tick
.mergeticklists(self
.manualticks
, newticks
)
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
]]
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
:
325 if worse
== self
.maxworse
and nextpart
== self
.parter
.lesspart
:
327 nextpart
= self
.parter
.morepart
328 if worse
== self
.maxworse
and nextpart
== self
.parter
.morepart
:
331 self
.ticks
=self
.manualticks
333 # rating, when several choises are available
336 if self
.painter
is not 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]
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
)
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
)
357 raise RuntimeError("no valid axis partitioning found")
358 variants
= [variant
for variant
in variants
[:i
] if variant
[0] is not None]
360 self
.ticks
= variants
[0][1]
362 self
.setrange(self
.ticks
[0], self
.ticks
[-1])
363 self
.axiscanvas
= variants
[0][2]
365 self
.ticks
= variants
[0][1]
366 self
.texter
.labels(self
.ticks
)
368 self
.setrange(self
.ticks
[0], self
.ticks
[-1])
369 self
.axiscanvas
= painter
.axiscanvas()
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
)
377 self
.axiscanvas
= painter
.axiscanvas()
379 # disable the axis divisor
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
397 - the rater implements _Irater and is used to rate different
398 tick lists created by the partitioner (after merging with
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
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
420 - the rater implements _Irater and is used to rate different
421 tick lists created by the partitioner (after merging with
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)
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
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
):
460 - instead of performing the hole finish process
461 (paritioning, rating, etc.) just a painter call
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
()
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
512 - title is the title of the axis as a string
513 - painter is the painter of the axis; it should be specialized to
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
520 self
.splitlist
= splitlist
521 for subaxis
in self
.subaxes
:
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
]
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
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
557 self
.fixmin
= self
.subaxes
[0].fixmin
559 self
.min = self
.subaxes
[0].min
560 self
.fixmax
= self
.subaxes
[-1].fixmax
562 self
.max = self
.subaxes
[-1].max
564 self
.axiscanvas
= None
567 min = self
.subaxes
[0].getrange()
568 max = self
.subaxes
[-1].getrange()
570 return min[0], max[1]
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
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
)
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())
626 self
.subaxes
.append(subaxis
.createlinkaxis(painter
=painter
))
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
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
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"""
676 if firstdist
is not None:
677 self
.firstdist
= firstdist
679 self
.firstdist
= 0.5 * dist
680 if lastdist
is not None:
681 self
.lastdist
= lastdist
683 self
.lastdist
= 0.5 * dist
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")
691 self
.subaxis
= subaxis
693 self
.painter
= painter
694 self
.axiscanvas
= None
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
)
707 # TODO: we do not yet have a proper range handling for a bar axis
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
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
:
723 self
.names
.append(name
)
724 if self
.multisubaxis
is not None:
725 self
.subaxis
.append(self
.createsubaxis())
727 if self
.multisubaxis
is not None:
728 if self
.subaxis
[self
.names
.index(name
)].setname(*subnames
):
731 if self
.subaxis
.setname(*subnames
):
733 return self
.relsizes
is not None
735 def adjustrange(self
, points
, index
, subnames
=None):
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:
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
752 if self
.subaxis
is None:
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
766 - following values are passed to the appropriate subaxis
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])
775 if self
.subaxis
is None:
778 if self
.multisubaxis
is not None:
779 subvalue
= value
[1] * self
.subaxis
[pos
].relsizes
[-1]
781 subvalue
= value
[1] * self
.subaxis
.relsizes
[-1]
783 if self
.multisubaxis
is not None:
784 subvalue
= self
.subaxis
[pos
].convert(value
[1:]) * self
.subaxis
[pos
].relsizes
[-1]
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
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