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 re
, math
, string
, sys
27 import attr
, bbox
, box
, canvas
, color
, deco
, helper
, path
, style
, unit
, mathtree
28 import text
as textmodule
29 import data
as datamodule
30 import trafo
as trafomodule
33 goldenmean
= 0.5 * (math
.sqrt(5) + 1)
36 ################################################################################
38 ################################################################################
41 """interface definition of a map
42 maps convert a value into another value by bijective transformation f"""
48 "returns f^-1(y) where f^-1 is the inverse transformation (x=f^-1(f(x)) for all x)"
50 def setbasepoints(self
, basepoints
):
51 """set basepoints for the convertions
52 basepoints are tuples (x, y) with y == f(x) and x == f^-1(y)
53 the number of basepoints needed might depend on the transformation
54 usually two pairs are needed like for linear maps, logarithmic maps, etc."""
60 __implements__
= _Imap
62 def setbasepoints(self
, basepoints
):
63 self
.dydx
= (basepoints
[1][1] - basepoints
[0][1]) / float(basepoints
[1][0] - basepoints
[0][0])
64 self
.dxdy
= (basepoints
[1][0] - basepoints
[0][0]) / float(basepoints
[1][1] - basepoints
[0][1])
65 self
.x1
= basepoints
[0][0]
66 self
.y1
= basepoints
[0][1]
68 def convert(self
, value
):
69 return self
.y1
+ self
.dydx
* (value
- self
.x1
)
71 def invert(self
, value
):
72 return self
.x1
+ self
.dxdy
* (value
- self
.y1
)
77 __implements__
= _Imap
79 def setbasepoints(self
, basepoints
):
80 self
.dydx
= ((basepoints
[1][1] - basepoints
[0][1]) /
81 float(math
.log(basepoints
[1][0]) - math
.log(basepoints
[0][0])))
82 self
.dxdy
= ((math
.log(basepoints
[1][0]) - math
.log(basepoints
[0][0])) /
83 float(basepoints
[1][1] - basepoints
[0][1]))
84 self
.x1
= math
.log(basepoints
[0][0])
85 self
.y1
= basepoints
[0][1]
88 def convert(self
, value
):
89 return self
.y1
+ self
.dydx
* (math
.log(value
) - self
.x1
)
91 def invert(self
, value
):
92 return math
.exp(self
.x1
+ self
.dxdy
* (value
- self
.y1
))
96 ################################################################################
98 # please note the nomenclature:
99 # - a partition is a list of tick instances; to reduce name clashes, a
100 # partition is called ticks
101 # - a partitioner is a class creating a single or several ticks
102 # - an axis has a part attribute where it stores a partitioner or/and some
103 # (manually set) ticks -> the part attribute is used to create the ticks
104 # in the axis finish method
105 ################################################################################
109 """fraction class for rational arithmetics
110 the axis partitioning uses rational arithmetics (with infinite accuracy)
111 basically it contains self.enum and self.denom"""
113 def stringfrac(self
, s
):
114 "converts a string 0.123 into a frac"
115 expparts
= s
.split("e")
116 if len(expparts
) > 2:
117 raise ValueError("multiple 'e' found in '%s'" % s
)
118 commaparts
= expparts
[0].split(".")
119 if len(commaparts
) > 2:
120 raise ValueError("multiple '.' found in '%s'" % expparts
[0])
121 if len(commaparts
) == 1:
122 commaparts
= [commaparts
[0], ""]
123 result
= frac((1, 10l), power
=len(commaparts
[1]))
124 neg
= len(commaparts
[0]) and commaparts
[0][0] == "-"
126 commaparts
[0] = commaparts
[0][1:]
127 elif len(commaparts
[0]) and commaparts
[0][0] == "+":
128 commaparts
[0] = commaparts
[0][1:]
129 if len(commaparts
[0]):
130 if not commaparts
[0].isdigit():
131 raise ValueError("unrecognized characters in '%s'" % s
)
132 x
= long(commaparts
[0])
135 if len(commaparts
[1]):
136 if not commaparts
[1].isdigit():
137 raise ValueError("unrecognized characters in '%s'" % s
)
138 y
= long(commaparts
[1])
141 result
.enum
= x
*result
.denom
+y
143 result
.enum
= -result
.enum
144 if len(expparts
) == 2:
145 neg
= expparts
[1][0] == "-"
147 expparts
[1] = expparts
[1][1:]
148 elif expparts
[1][0] == "+":
149 expparts
[1] = expparts
[1][1:]
150 if not expparts
[1].isdigit():
151 raise ValueError("unrecognized characters in '%s'" % s
)
153 result
*= frac((1, 10l), power
=long(expparts
[1]))
155 result
*= frac((10, 1l), power
=long(expparts
[1]))
158 def floatfrac(self
, x
, floatprecision
):
159 "converts a float into a frac with finite resolution"
160 if helper
.isinteger(floatprecision
) and floatprecision
< 0:
161 # this would be extremly vulnerable
162 raise RuntimeError("float resolution must be non-negative integer")
163 return self
.stringfrac(("%%.%ig" % floatprecision
) % x
)
165 def __init__(self
, x
, power
=None, floatprecision
=10):
166 "for power!=None: frac=(enum/denom)**power"
167 if helper
.isnumber(x
):
168 value
= self
.floatfrac(x
, floatprecision
)
169 enum
, denom
= value
.enum
, value
.denom
170 elif helper
.isstring(x
):
171 fraction
= x
.split("/")
172 if len(fraction
) > 2:
173 raise ValueError("multiple '/' found in '%s'" % x
)
174 value
= self
.stringfrac(fraction
[0])
175 if len(fraction
) == 2:
176 value2
= self
.stringfrac(fraction
[1])
177 value
= value
/ value2
178 enum
, denom
= value
.enum
, value
.denom
182 except (TypeError, AttributeError):
183 enum
, denom
= x
.enum
, x
.denom
184 if not helper
.isinteger(enum
) or not helper
.isinteger(denom
): raise TypeError("integer type expected")
185 if not denom
: raise ZeroDivisionError("zero denominator")
187 if not helper
.isinteger(power
): raise TypeError("integer type expected")
189 self
.enum
= long(enum
) ** power
190 self
.denom
= long(denom
) ** power
192 self
.enum
= long(denom
) ** (-power
)
193 self
.denom
= long(enum
) ** (-power
)
198 def __cmp__(self
, other
):
201 return cmp(self
.enum
* other
.denom
, other
.enum
* self
.denom
)
204 return frac((abs(self
.enum
), abs(self
.denom
)))
206 def __mul__(self
, other
):
207 return frac((self
.enum
* other
.enum
, self
.denom
* other
.denom
))
209 def __div__(self
, other
):
210 return frac((self
.enum
* other
.denom
, self
.denom
* other
.enum
))
213 "caution: avoid final precision of floats"
214 return float(self
.enum
) / self
.denom
217 return "%i/%i" % (self
.enum
, self
.denom
)
222 a tick is a frac enhanced by
223 - self.ticklevel (0 = tick, 1 = subtick, etc.)
224 - self.labellevel (0 = label, 1 = sublabel, etc.)
225 - self.label (a string) and self.labelattrs (a list, defaults to [])
226 When ticklevel or labellevel is None, no tick or label is present at that value.
227 When label is None, it should be automatically created (and stored), once the
228 an axis painter needs it. Classes, which implement _Itexter do precisely that."""
230 def __init__(self
, pos
, ticklevel
=0, labellevel
=0, label
=None, labelattrs
=[], **kwargs
):
231 """initializes the instance
232 - see class description for the parameter description
233 - **kwargs are passed to the frac constructor"""
234 frac
.__init
__(self
, pos
, **kwargs
)
235 self
.ticklevel
= ticklevel
236 self
.labellevel
= labellevel
238 self
.labelattrs
= labelattrs
240 def merge(self
, other
):
241 """merges two ticks together:
242 - the lower ticklevel/labellevel wins
243 - the label is *never* taken over from other
244 - the ticks should be at the same position (otherwise it doesn't make sense)
245 -> this is NOT checked"""
246 if self
.ticklevel
is None or (other
.ticklevel
is not None and other
.ticklevel
< self
.ticklevel
):
247 self
.ticklevel
= other
.ticklevel
248 if self
.labellevel
is None or (other
.labellevel
is not None and other
.labellevel
< self
.labellevel
):
249 self
.labellevel
= other
.labellevel
252 def _mergeticklists(list1
, list2
):
253 """helper function to merge tick lists
254 - return a merged list of ticks out of list1 and list2
255 - CAUTION: original lists have to be ordered
256 (the returned list is also ordered)"""
257 # TODO: improve this using bisect?!
259 # XXX do not the original lists
264 while 1: # we keep on going until we reach an index error
265 while list2
[j
] < list1
[i
]: # insert tick
266 list1
.insert(i
, list2
[j
])
269 if list2
[j
] == list1
[i
]: # merge tick
270 list1
[i
].merge(list2
[j
])
279 def _mergelabels(ticks
, labels
):
280 """helper function to merge labels into ticks
281 - when labels is not None, the label of all ticks with
282 labellevel different from None are set
283 - labels need to be a list of lists of strings,
284 where the first list contain the strings to be
285 used as labels for the ticks with labellevel 0,
286 the second list for labellevel 1, etc.
287 - when the maximum labellevel is 0, just a list of
288 strings might be provided as the labels argument
289 - IndexError is raised, when a list length doesn't match"""
290 if helper
.issequenceofsequences(labels
):
291 for label
, level
in zip(labels
, xrange(sys
.maxint
)):
292 usetext
= helper
.ensuresequence(label
)
295 if tick
.labellevel
== level
:
296 tick
.label
= usetext
[i
]
298 if i
!= len(usetext
):
299 raise IndexError("wrong list length of labels at level %i" % level
)
300 elif labels
is not None:
301 usetext
= helper
.ensuresequence(labels
)
304 if tick
.labellevel
== 0:
305 tick
.label
= usetext
[i
]
307 if i
!= len(usetext
):
308 raise IndexError("wrong list length of labels")
310 def _maxlevels(ticks
):
311 "returns a tuple maxticklist, maxlabellevel from a list of tick instances"
312 maxticklevel
= maxlabellevel
= 0
314 if tick
.ticklevel
is not None and tick
.ticklevel
>= maxticklevel
:
315 maxticklevel
= tick
.ticklevel
+ 1
316 if tick
.labellevel
is not None and tick
.labellevel
>= maxlabellevel
:
317 maxlabellevel
= tick
.labellevel
+ 1
318 return maxticklevel
, maxlabellevel
322 """interface definition of a partition scheme
323 partition schemes are used to create a list of ticks"""
325 def defaultpart(self
, min, max, extendmin
, extendmax
):
326 """create a partition
327 - returns an ordered list of ticks for the interval min to max
328 - the interval is given in float numbers, thus an appropriate
329 conversion to rational numbers has to be performed
330 - extendmin and extendmax are booleans (integers)
331 - when extendmin or extendmax is set, the ticks might
332 extend the min-max range towards lower and higher
333 ranges, respectively"""
336 """create another partition which contains less ticks
337 - this method is called several times after a call of defaultpart
338 - returns an ordered list of ticks with less ticks compared to
339 the partition returned by defaultpart and by previous calls
341 - the creation of a partition with strictly *less* ticks
342 is not to be taken serious
343 - the method might return None, when no other appropriate
344 partition can be created"""
348 """create another partition which contains more ticks
349 see lesspart, but increase the number of ticks"""
353 """linear partition scheme
354 ticks and label distances are explicitly provided to the constructor"""
356 __implements__
= _Iparter
358 def __init__(self
, tickdist
=None, labeldist
=None, labels
=None, extendtick
=0, extendlabel
=None, epsilon
=1e-10):
359 """configuration of the partition scheme
360 - tickdist and labeldist should be a list, where the first value
361 is the distance between ticks with ticklevel/labellevel 0,
362 the second list for ticklevel/labellevel 1, etc.;
363 a single entry is allowed without being a list
364 - tickdist and labeldist values are passed to the frac constructor
365 - when labeldist is None and tickdist is not None, the tick entries
366 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
367 - labels are applied to the resulting partition via the
368 mergelabels function (additional information available there)
369 - extendtick allows for the extension of the range given to the
370 defaultpart method to include the next tick with the specified
371 level (None turns off this feature); note, that this feature is
372 also disabled, when an axis prohibits its range extension by
373 the extendmin/extendmax variables given to the defaultpart method
374 - extendlabel is analogous to extendtick, but for labels
375 - epsilon allows for exceeding the axis range by this relative
376 value (relative to the axis range given to the defaultpart method)
377 without creating another tick specified by extendtick/extendlabel"""
378 if tickdist
is None and labeldist
is not None:
379 self
.ticklist
= (frac(helper
.ensuresequence(labeldist
)[0]),)
381 self
.ticklist
= map(frac
, helper
.ensuresequence(tickdist
))
382 if labeldist
is None and tickdist
is not None:
383 self
.labellist
= (frac(helper
.ensuresequence(tickdist
)[0]),)
385 self
.labellist
= map(frac
, helper
.ensuresequence(labeldist
))
387 self
.extendtick
= extendtick
388 self
.extendlabel
= extendlabel
389 self
.epsilon
= epsilon
391 def extendminmax(self
, min, max, frac
, extendmin
, extendmax
):
392 """return new min, max tuple extending the range min, max
393 - frac is the tick distance to be used
394 - extendmin and extendmax are booleans to allow for the extension"""
396 min = float(frac
) * math
.floor(min / float(frac
) + self
.epsilon
)
398 max = float(frac
) * math
.ceil(max / float(frac
) - self
.epsilon
)
401 def getticks(self
, min, max, frac
, ticklevel
=None, labellevel
=None):
402 """return a list of equal spaced ticks
403 - the tick distance is frac, the ticklevel is set to ticklevel and
404 the labellevel is set to labellevel
405 - min, max is the range where ticks should be placed"""
406 imin
= int(math
.ceil(min / float(frac
) - 0.5 * self
.epsilon
))
407 imax
= int(math
.floor(max / float(frac
) + 0.5 * self
.epsilon
))
409 for i
in range(imin
, imax
+ 1):
410 ticks
.append(tick((long(i
) * frac
.enum
, frac
.denom
), ticklevel
=ticklevel
, labellevel
=labellevel
))
413 def defaultpart(self
, min, max, extendmin
, extendmax
):
414 if self
.extendtick
is not None and len(self
.ticklist
) > self
.extendtick
:
415 min, max = self
.extendminmax(min, max, self
.ticklist
[self
.extendtick
], extendmin
, extendmax
)
416 if self
.extendlabel
is not None and len(self
.labellist
) > self
.extendlabel
:
417 min, max = self
.extendminmax(min, max, self
.labellist
[self
.extendlabel
], extendmin
, extendmax
)
420 for i
in range(len(self
.ticklist
)):
421 ticks
= _mergeticklists(ticks
, self
.getticks(min, max, self
.ticklist
[i
], ticklevel
= i
))
422 for i
in range(len(self
.labellist
)):
423 ticks
= _mergeticklists(ticks
, self
.getticks(min, max, self
.labellist
[i
], labellevel
= i
))
425 _mergelabels(ticks
, self
.labels
)
437 """automatic linear partition scheme
438 - possible tick distances are explicitly provided to the constructor
439 - tick distances are adjusted to the axis range by multiplication or division by 10"""
441 __implements__
= _Iparter
443 defaultvariants
= ((frac((1, 1)), frac((1, 2))),
444 (frac((2, 1)), frac((1, 1))),
445 (frac((5, 2)), frac((5, 4))),
446 (frac((5, 1)), frac((5, 2))))
448 def __init__(self
, variants
=defaultvariants
, extendtick
=0, epsilon
=1e-10):
449 """configuration of the partition scheme
450 - variants is a list of tickdist
451 - tickdist should be a list, where the first value
452 is the distance between ticks with ticklevel 0,
453 the second for ticklevel 1, etc.
454 - tickdist values are passed to the frac constructor
455 - labellevel is set to None except for those ticks in the partitions,
456 where ticklevel is zero. There labellevel is also set to zero.
457 - extendtick allows for the extension of the range given to the
458 defaultpart method to include the next tick with the specified
459 level (None turns off this feature); note, that this feature is
460 also disabled, when an axis prohibits its range extension by
461 the extendmin/extendmax variables given to the defaultpart method
462 - epsilon allows for exceeding the axis range by this relative
463 value (relative to the axis range given to the defaultpart method)
464 without creating another tick specified by extendtick"""
465 self
.variants
= variants
466 self
.extendtick
= extendtick
467 self
.epsilon
= epsilon
469 def defaultpart(self
, min, max, extendmin
, extendmax
):
470 logmm
= math
.log(max - min) / math
.log(10)
471 if logmm
< 0: # correction for rounding towards zero of the int routine
472 base
= frac((10L, 1), int(logmm
- 1))
474 base
= frac((10L, 1), int(logmm
))
475 ticks
= map(frac
, self
.variants
[0])
476 useticks
= [tick
* base
for tick
in ticks
]
477 self
.lesstickindex
= self
.moretickindex
= 0
478 self
.lessbase
= frac((base
.enum
, base
.denom
))
479 self
.morebase
= frac((base
.enum
, base
.denom
))
480 self
.min, self
.max, self
.extendmin
, self
.extendmax
= min, max, extendmin
, extendmax
481 part
= linparter(tickdist
=useticks
, extendtick
=self
.extendtick
, epsilon
=self
.epsilon
)
482 return part
.defaultpart(self
.min, self
.max, self
.extendmin
, self
.extendmax
)
485 if self
.lesstickindex
< len(self
.variants
) - 1:
486 self
.lesstickindex
+= 1
488 self
.lesstickindex
= 0
489 self
.lessbase
.enum
*= 10
490 ticks
= map(frac
, self
.variants
[self
.lesstickindex
])
491 useticks
= [tick
* self
.lessbase
for tick
in ticks
]
492 part
= linparter(tickdist
=useticks
, extendtick
=self
.extendtick
, epsilon
=self
.epsilon
)
493 return part
.defaultpart(self
.min, self
.max, self
.extendmin
, self
.extendmax
)
496 if self
.moretickindex
:
497 self
.moretickindex
-= 1
499 self
.moretickindex
= len(self
.variants
) - 1
500 self
.morebase
.denom
*= 10
501 ticks
= map(frac
, self
.variants
[self
.moretickindex
])
502 useticks
= [tick
* self
.morebase
for tick
in ticks
]
503 part
= linparter(tickdist
=useticks
, extendtick
=self
.extendtick
, epsilon
=self
.epsilon
)
504 return part
.defaultpart(self
.min, self
.max, self
.extendmin
, self
.extendmax
)
508 """storage class for the definition of logarithmic axes partitions
509 instances of this class define tick positions suitable for
510 logarithmic axes by the following instance variables:
511 - exp: integer, which defines multiplicator (usually 10)
512 - pres: list of tick positions (rational numbers, e.g. instances of frac)
513 possible positions are these tick positions and arbitrary divisions
514 and multiplications by the exp value"""
516 def __init__(self
, pres
, exp
):
517 "create a preexp instance and store its pres and exp information"
518 self
.pres
= helper
.ensuresequence(pres
)
522 class logparter(linparter
):
523 """logarithmic partition scheme
524 ticks and label positions are explicitly provided to the constructor"""
526 __implements__
= _Iparter
528 pre1exp5
= preexp(frac((1, 1)), 100000)
529 pre1exp4
= preexp(frac((1, 1)), 10000)
530 pre1exp3
= preexp(frac((1, 1)), 1000)
531 pre1exp2
= preexp(frac((1, 1)), 100)
532 pre1exp
= preexp(frac((1, 1)), 10)
533 pre125exp
= preexp((frac((1, 1)), frac((2, 1)), frac((5, 1))), 10)
534 pre1to9exp
= preexp(map(lambda x
: frac((x
, 1)), range(1, 10)), 10)
535 # ^- we always include 1 in order to get extendto(tick|label)level to work as expected
537 def __init__(self
, tickpos
=None, labelpos
=None, labels
=None, extendtick
=0, extendlabel
=None, epsilon
=1e-10):
538 """configuration of the partition scheme
539 - tickpos and labelpos should be a list, where the first entry
540 is a preexp instance describing ticks with ticklevel/labellevel 0,
541 the second is a preexp instance for ticklevel/labellevel 1, etc.;
542 a single entry is allowed without being a list
543 - when labelpos is None and tickpos is not None, the tick entries
544 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
545 - labels are applied to the resulting partition via the
546 mergetexts function (additional information available there)
547 - extendtick allows for the extension of the range given to the
548 defaultpart method to include the next tick with the specified
549 level (None turns off this feature); note, that this feature is
550 also disabled, when an axis prohibits its range extension by
551 the extendmin/extendmax variables given to the defaultpart method
552 - extendlabel is analogous to extendtick, but for labels
553 - epsilon allows for exceeding the axis range by this relative
554 logarithm value (relative to the logarithm axis range given
555 to the defaultpart method) without creating another tick
556 specified by extendtick/extendlabel"""
557 if tickpos
is None and labels
is not None:
558 self
.ticklist
= (helper
.ensuresequence(labelpos
)[0],)
560 self
.ticklist
= helper
.ensuresequence(tickpos
)
562 if labelpos
is None and tickpos
is not None:
563 self
.labellist
= (helper
.ensuresequence(tickpos
)[0],)
565 self
.labellist
= helper
.ensuresequence(labelpos
)
567 self
.extendtick
= extendtick
568 self
.extendlabel
= extendlabel
569 self
.epsilon
= epsilon
571 def extendminmax(self
, min, max, preexp
, extendmin
, extendmax
):
572 """return new min, max tuple extending the range min, max
573 preexp describes the allowed tick positions
574 extendmin and extendmax are booleans to allow for the extension"""
577 for i
in xrange(len(preexp
.pres
)):
578 imin
= int(math
.floor(math
.log(min / float(preexp
.pres
[i
])) /
579 math
.log(preexp
.exp
) + self
.epsilon
)) + 1
580 imax
= int(math
.ceil(math
.log(max / float(preexp
.pres
[i
])) /
581 math
.log(preexp
.exp
) - self
.epsilon
)) - 1
582 if minpower
is None or imin
< minpower
:
583 minpower
, minindex
= imin
, i
584 if maxpower
is None or imax
>= maxpower
:
585 maxpower
, maxindex
= imax
, i
587 minfrac
= preexp
.pres
[minindex
- 1]
589 minfrac
= preexp
.pres
[-1]
591 if maxindex
!= len(preexp
.pres
) - 1:
592 maxfrac
= preexp
.pres
[maxindex
+ 1]
594 maxfrac
= preexp
.pres
[0]
597 min = float(minfrac
) * float(preexp
.exp
) ** minpower
599 max = float(maxfrac
) * float(preexp
.exp
) ** maxpower
602 def getticks(self
, min, max, preexp
, ticklevel
=None, labellevel
=None):
603 """return a list of ticks
604 - preexp describes the allowed tick positions
605 - the ticklevel of the ticks is set to ticklevel and
606 the labellevel is set to labellevel
607 - min, max is the range where ticks should be placed"""
611 for f
in preexp
.pres
:
613 imin
= int(math
.ceil(math
.log(min / float(f
)) /
614 math
.log(preexp
.exp
) - 0.5 * self
.epsilon
))
615 imax
= int(math
.floor(math
.log(max / float(f
)) /
616 math
.log(preexp
.exp
) + 0.5 * self
.epsilon
))
617 for i
in range(imin
, imax
+ 1):
618 pos
= f
* frac((preexp
.exp
, 1), i
)
619 fracticks
.append(tick((pos
.enum
, pos
.denom
), ticklevel
= ticklevel
, labellevel
= labellevel
))
620 ticks
= _mergeticklists(ticks
, fracticks
)
624 class autologparter(logparter
):
625 """automatic logarithmic partition scheme
626 possible tick positions are explicitly provided to the constructor"""
628 __implements__
= _Iparter
630 defaultvariants
= (((logparter
.pre1exp
, # ticks
631 logparter
.pre1to9exp
), # subticks
632 (logparter
.pre1exp
, # labels
633 logparter
.pre125exp
)), # sublevels
635 ((logparter
.pre1exp
, # ticks
636 logparter
.pre1to9exp
), # subticks
637 None), # labels like ticks
639 ((logparter
.pre1exp2
, # ticks
640 logparter
.pre1exp
), # subticks
641 None), # labels like ticks
643 ((logparter
.pre1exp3
, # ticks
644 logparter
.pre1exp
), # subticks
645 None), # labels like ticks
647 ((logparter
.pre1exp4
, # ticks
648 logparter
.pre1exp
), # subticks
649 None), # labels like ticks
651 ((logparter
.pre1exp5
, # ticks
652 logparter
.pre1exp
), # subticks
653 None)) # labels like ticks
655 def __init__(self
, variants
=defaultvariants
, extendtick
=0, extendlabel
=None, epsilon
=1e-10):
656 """configuration of the partition scheme
657 - variants should be a list of pairs of lists of preexp
659 - within each pair the first list contains preexp, where
660 the first preexp instance describes ticks positions with
661 ticklevel 0, the second preexp for ticklevel 1, etc.
662 - the second list within each pair describes the same as
663 before, but for labels
664 - within each pair: when the second entry (for the labels) is None
665 and the first entry (for the ticks) ticks is not None, the tick
666 entries for ticklevel 0 are used for labels and vice versa
668 - extendtick allows for the extension of the range given to the
669 defaultpart method to include the next tick with the specified
670 level (None turns off this feature); note, that this feature is
671 also disabled, when an axis prohibits its range extension by
672 the extendmin/extendmax variables given to the defaultpart method
673 - extendlabel is analogous to extendtick, but for labels
674 - epsilon allows for exceeding the axis range by this relative
675 logarithm value (relative to the logarithm axis range given
676 to the defaultpart method) without creating another tick
677 specified by extendtick/extendlabel"""
678 self
.variants
= variants
679 if len(variants
) > 2:
680 self
.variantsindex
= divmod(len(variants
), 2)[0]
682 self
.variantsindex
= 0
683 self
.extendtick
= extendtick
684 self
.extendlabel
= extendlabel
685 self
.epsilon
= epsilon
687 def defaultpart(self
, min, max, extendmin
, extendmax
):
688 self
.min, self
.max, self
.extendmin
, self
.extendmax
= min, max, extendmin
, extendmax
689 self
.morevariantsindex
= self
.variantsindex
690 self
.lessvariantsindex
= self
.variantsindex
691 part
= logparter(tickpos
=self
.variants
[self
.variantsindex
][0], labelpos
=self
.variants
[self
.variantsindex
][1],
692 extendtick
=self
.extendtick
, extendlabel
=self
.extendlabel
, epsilon
=self
.epsilon
)
693 return part
.defaultpart(self
.min, self
.max, self
.extendmin
, self
.extendmax
)
696 self
.lessvariantsindex
+= 1
697 if self
.lessvariantsindex
< len(self
.variants
):
698 part
= logparter(tickpos
=self
.variants
[self
.lessvariantsindex
][0], labelpos
=self
.variants
[self
.lessvariantsindex
][1],
699 extendtick
=self
.extendtick
, extendlabel
=self
.extendlabel
, epsilon
=self
.epsilon
)
700 return part
.defaultpart(self
.min, self
.max, self
.extendmin
, self
.extendmax
)
703 self
.morevariantsindex
-= 1
704 if self
.morevariantsindex
>= 0:
705 part
= logparter(tickpos
=self
.variants
[self
.morevariantsindex
][0], labelpos
=self
.variants
[self
.morevariantsindex
][1],
706 extendtick
=self
.extendtick
, extendlabel
=self
.extendlabel
, epsilon
=self
.epsilon
)
707 return part
.defaultpart(self
.min, self
.max, self
.extendmin
, self
.extendmax
)
711 ################################################################################
713 # conseptional remarks:
714 # - raters are used to calculate a rating for a realization of something
715 # - here, a rating means a positive floating point value
716 # - ratings are used to order those realizations by their suitability (lower
717 # ratings are better)
718 # - a rating of None means not suitable at all (those realizations should be
720 ################################################################################
725 - a cube rater has an optimal value, where the rate becomes zero
726 - for a left (below the optimum) and a right value (above the optimum),
727 the rating is value is set to 1 (modified by an overall weight factor
729 - the analytic form of the rating is cubic for both, the left and
730 the right side of the rater, independently"""
732 # __implements__ = sole implementation
734 def __init__(self
, opt
, left
=None, right
=None, weight
=1):
735 """initializes the rater
736 - by default, left is set to zero, right is set to 3*opt
737 - left should be smaller than opt, right should be bigger than opt
738 - weight should be positive and is a factor multiplicated to the rates"""
748 def rate(self
, value
, density
):
749 """returns a rating for a value
750 - the density lineary rescales the rater (the optimum etc.),
751 e.g. a value bigger than one increases the optimum (when it is
752 positive) and a value lower than one decreases the optimum (when
753 it is positive); the density itself should be positive"""
754 opt
= self
.opt
* density
756 other
= self
.left
* density
758 other
= self
.right
* density
761 factor
= (value
- opt
) / float(other
- opt
)
762 return self
.weight
* (factor
** 3)
766 # TODO: update docstring
767 """a distance rater (rates a list of distances)
768 - the distance rater rates a list of distances by rating each independently
769 and returning the average rate
770 - there is an optimal value, where the rate becomes zero
771 - the analytic form is linary for values above the optimal value
772 (twice the optimal value has the rating one, three times the optimal
773 value has the rating two, etc.)
774 - the analytic form is reciprocal subtracting one for values below the
775 optimal value (halve the optimal value has the rating one, one third of
776 the optimal value has the rating two, etc.)"""
778 # __implements__ = sole implementation
780 def __init__(self
, opt
, weight
=0.1):
781 """inititializes the rater
782 - opt is the optimal length (a visual PyX length)
783 - weight should be positive and is a factor multiplicated to the rates"""
787 def rate(self
, distances
, density
):
789 - the distances are a list of positive floats in PostScript points
790 - the density lineary rescales the rater (the optimum etc.),
791 e.g. a value bigger than one increases the optimum (when it is
792 positive) and a value lower than one decreases the optimum (when
793 it is positive); the density itself should be positive"""
795 opt
= unit
.topt(unit
.length(self
.opt_str
, default_type
="v")) / density
797 for distance
in distances
:
799 rate
+= self
.weight
* (opt
/ distance
- 1)
801 rate
+= self
.weight
* (distance
/ opt
- 1)
802 return rate
/ float(len(distances
))
807 - the rating of axes is splited into two separate parts:
808 - rating of the ticks in terms of the number of ticks, subticks,
810 - rating of the label distances
811 - in the end, a rate for ticks is the sum of these rates
812 - it is useful to first just rate the number of ticks etc.
813 and selecting those partitions, where this fits well -> as soon
814 as an complete rate (the sum of both parts from the list above)
815 of a first ticks is below a rate of just the number of ticks,
816 subticks labels etc. of other ticks, those other ticks will never
817 be better than the first one -> we gain speed by minimizing the
818 number of ticks, where label distances have to be taken into account)
819 - both parts of the rating are shifted into instances of raters
820 defined above --- right now, there is not yet a strict interface
821 for this delegation (should be done as soon as it is needed)"""
823 # __implements__ = sole implementation
825 linticks
= (cuberater(4), cuberater(10, weight
=0.5), )
826 linlabels
= (cuberater(4), )
827 logticks
= (cuberater(5, right
=20), cuberater(20, right
=100, weight
=0.5), )
828 loglabels
= (cuberater(5, right
=20), cuberater(5, left
=-20, right
=20, weight
=0.5), )
829 stdrange
= cuberater(1, weight
=2)
830 stddistance
= distancerater("1 cm")
832 def __init__(self
, ticks
=linticks
, labels
=linlabels
, range=stdrange
, distance
=stddistance
):
833 """initializes the axis rater
834 - ticks and labels are lists of instances of a value rater
835 - the first entry in ticks rate the number of ticks, the
836 second the number of subticks, etc.; when there are no
837 ticks of a level or there is not rater for a level, the
838 level is just ignored
839 - labels is analogous, but for labels
840 - within the rating, all ticks with a higher level are
841 considered as ticks for a given level
842 - range is a value rater instance, which rates the covering
843 of an axis range by the ticks (as a relative value of the
844 tick range vs. the axis range), ticks might cover less or
845 more than the axis range (for the standard automatic axis
846 partition schemes an extention of the axis range is normal
847 and should get some penalty)
848 - distance is an distance rater instance"""
852 self
.distance
= distance
854 def rateticks(self
, axis
, ticks
, density
):
855 """rates ticks by the number of ticks, subticks, labels etc.
856 - takes into account the number of ticks, subticks, labels
857 etc. and the coverage of the axis range by the ticks
858 - when there are no ticks of a level or there was not rater
859 given in the constructor for a level, the level is just
861 - the method returns the sum of the rating results divided
862 by the sum of the weights of the raters
863 - within the rating, all ticks with a higher level are
864 considered as ticks for a given level"""
865 maxticklevel
, maxlabellevel
= _maxlevels(ticks
)
866 numticks
= [0]*maxticklevel
867 numlabels
= [0]*maxlabellevel
869 if tick
.ticklevel
is not None:
870 for level
in range(tick
.ticklevel
, maxticklevel
):
872 if tick
.labellevel
is not None:
873 for level
in range(tick
.labellevel
, maxlabellevel
):
874 numlabels
[level
] += 1
877 for numtick
, rater
in zip(numticks
, self
.ticks
):
878 rate
+= rater
.rate(numtick
, density
)
879 weight
+= rater
.weight
880 for numlabel
, rater
in zip(numlabels
, self
.labels
):
881 rate
+= rater
.rate(numlabel
, density
)
882 weight
+= rater
.weight
885 def raterange(self
, tickrange
, datarange
):
886 """rate the range covered by the ticks compared to the range
888 - tickrange and datarange are the ranges covered by the ticks
889 and the data in graph coordinates
890 - usually, the datarange is 1 (ticks are calculated for a
892 - the ticks might cover less or more than the data range (for
893 the standard automatic axis partition schemes an extention
894 of the axis range is normal and should get some penalty)"""
895 return self
.range.rate(tickrange
, datarange
)
897 def ratelayout(self
, axiscanvas
, density
):
898 """rate distances of the labels in an axis canvas
899 - the distances should be collected as box distances of
901 - the axiscanvas provides a labels attribute for easy
902 access to the labels whose distances have to be taken
904 - the density is used within the distancerate instance"""
905 if len(axiscanvas
.labels
) > 1:
907 distances
= [axiscanvas
.labels
[i
].boxdistance_pt(axiscanvas
.labels
[i
+1]) for i
in range(len(axiscanvas
.labels
) - 1)]
908 except box
.BoxCrossError
:
910 return self
.distance
.rate(distances
, density
)
915 ################################################################################
917 # texter automatically create labels for tick instances
918 ################################################################################
923 def labels(self
, ticks
):
924 """fill the label attribute of ticks
925 - ticks is a list of instances of tick
926 - for each element of ticks the value of the attribute label is set to
927 a string appropriate to the attributes enum and denom of that tick
929 - label attributes of the tick instances are just kept, whenever they
930 are not equal to None
931 - the method might modify the labelattrs attribute of the ticks; be sure
932 to not modify it in-place!"""
935 class rationaltexter
:
936 "a texter creating rational labels (e.g. 'a/b' or even 'a \over b')"
937 # XXX: we use divmod here to be more expicit
939 __implements__
= _Itexter
941 def __init__(self
, prefix
="", infix
="", suffix
="",
942 enumprefix
="", enuminfix
="", enumsuffix
="",
943 denomprefix
="", denominfix
="", denomsuffix
="",
944 plus
="", minus
="-", minuspos
=0, over
=r
"{{%s}\over{%s}}",
945 equaldenom
=0, skip1
=1, skipenum0
=1, skipenum1
=1, skipdenom1
=1,
946 labelattrs
=[textmodule
.mathmode
]):
947 r
"""initializes the instance
948 - prefix, infix, and suffix (strings) are added at the begin,
949 immediately after the minus, and at the end of the label,
951 - prefixenum, infixenum, and suffixenum (strings) are added
952 to the labels enumerator correspondingly
953 - prefixdenom, infixdenom, and suffixdenom (strings) are added
954 to the labels denominator correspondingly
955 - plus or minus (string) is inserted for non-negative or negative numbers
956 - minuspos is an integer, which determines the position, where the
957 plus or minus sign has to be placed; the following values are allowed:
958 1 - writes the plus or minus in front of the enumerator
959 0 - writes the plus or minus in front of the hole fraction
960 -1 - writes the plus or minus in front of the denominator
961 - over (string) is taken as a format string generating the
962 fraction bar; it has to contain exactly two string insert
963 operators "%s" -- the first for the enumerator and the second
964 for the denominator; by far the most common examples are
965 r"{{%s}\over{%s}}" and "{{%s}/{%s}}"
966 - usually the enumerator and denominator are canceled; however,
967 when equaldenom is set, the least common multiple of all
969 - skip1 (boolean) just prints the prefix, the plus or minus,
970 the infix and the suffix, when the value is plus or minus one
971 and at least one of prefix, infix and the suffix is present
972 - skipenum0 (boolean) just prints a zero instead of
973 the hole fraction, when the enumerator is zero;
974 no prefixes, infixes, and suffixes are taken into account
975 - skipenum1 (boolean) just prints the enumprefix, the plus or minus,
976 the enuminfix and the enumsuffix, when the enum value is plus or minus one
977 and at least one of enumprefix, enuminfix and the enumsuffix is present
978 - skipdenom1 (boolean) just prints the enumerator instead of
979 the hole fraction, when the denominator is one and none of the parameters
980 denomprefix, denominfix and denomsuffix are set and minuspos is not -1 or the
982 - labelattrs is a list of attributes for a texrunners text method;
983 None is considered as an empty list; labelattrs might be changed
984 in the painter as well"""
988 self
.enumprefix
= enumprefix
989 self
.enuminfix
= enuminfix
990 self
.enumsuffix
= enumsuffix
991 self
.denomprefix
= denomprefix
992 self
.denominfix
= denominfix
993 self
.denomsuffix
= denomsuffix
996 self
.minuspos
= minuspos
998 self
.equaldenom
= equaldenom
1000 self
.skipenum0
= skipenum0
1001 self
.skipenum1
= skipenum1
1002 self
.skipdenom1
= skipdenom1
1003 self
.labelattrs
= labelattrs
1006 """returns the greates common divisor of all elements in n
1007 - the elements of n must be non-negative integers
1008 - return None if the number of elements is zero
1009 - the greates common divisor is not affected when some
1010 of the elements are zero, but it becomes zero when
1011 all elements are zero"""
1017 i
, (dummy
, j
) = j
, divmod(i
, j
)
1022 res
= self
.gcd(res
, i
)
1026 """returns the least common multiple of all elements in n
1027 - the elements of n must be non-negative integers
1028 - return None if the number of elements is zero
1029 - the least common multiple is zero when some of the
1030 elements are zero"""
1034 res
= divmod(res
* i
, self
.gcd(res
, i
))[0]
1037 def labels(self
, ticks
):
1040 if tick
.label
is None and tick
.labellevel
is not None:
1041 labeledticks
.append(tick
)
1042 tick
.temp_fracenum
= tick
.enum
1043 tick
.temp_fracdenom
= tick
.denom
1044 tick
.temp_fracminus
= 1
1045 if tick
.temp_fracenum
< 0:
1046 tick
.temp_fracminus
= -tick
.temp_fracminus
1047 tick
.temp_fracenum
= -tick
.temp_fracenum
1048 if tick
.temp_fracdenom
< 0:
1049 tick
.temp_fracminus
= -tick
.temp_fracminus
1050 tick
.temp_fracdenom
= -tick
.temp_fracdenom
1051 gcd
= self
.gcd(tick
.temp_fracenum
, tick
.temp_fracdenom
)
1052 (tick
.temp_fracenum
, dummy1
), (tick
.temp_fracdenom
, dummy2
) = divmod(tick
.temp_fracenum
, gcd
), divmod(tick
.temp_fracdenom
, gcd
)
1054 equaldenom
= self
.lcm(*[tick
.temp_fracdenom
for tick
in ticks
if tick
.label
is None])
1055 if equaldenom
is not None:
1056 for tick
in labeledticks
:
1057 factor
, dummy
= divmod(equaldenom
, tick
.temp_fracdenom
)
1058 tick
.temp_fracenum
, tick
.temp_fracdenom
= factor
* tick
.temp_fracenum
, factor
* tick
.temp_fracdenom
1059 for tick
in labeledticks
:
1060 fracminus
= fracenumminus
= fracdenomminus
= ""
1061 if tick
.temp_fracminus
== -1:
1062 plusminus
= self
.minus
1064 plusminus
= self
.plus
1065 if self
.minuspos
== 0:
1066 fracminus
= plusminus
1067 elif self
.minuspos
== 1:
1068 fracenumminus
= plusminus
1069 elif self
.minuspos
== -1:
1070 fracdenomminus
= plusminus
1072 raise RuntimeError("invalid minuspos")
1073 if self
.skipenum0
and tick
.temp_fracenum
== 0:
1075 elif (self
.skip1
and self
.skipdenom1
and tick
.temp_fracenum
== 1 and tick
.temp_fracdenom
== 1 and
1076 (len(self
.prefix
) or len(self
.infix
) or len(self
.suffix
)) and
1077 not len(fracenumminus
) and not len(self
.enumprefix
) and not len(self
.enuminfix
) and not len(self
.enumsuffix
) and
1078 not len(fracdenomminus
) and not len(self
.denomprefix
) and not len(self
.denominfix
) and not len(self
.denomsuffix
)):
1079 tick
.label
= "%s%s%s%s" % (self
.prefix
, fracminus
, self
.infix
, self
.suffix
)
1081 if self
.skipenum1
and tick
.temp_fracenum
== 1 and (len(self
.enumprefix
) or len(self
.enuminfix
) or len(self
.enumsuffix
)):
1082 tick
.temp_fracenum
= "%s%s%s%s" % (self
.enumprefix
, fracenumminus
, self
.enuminfix
, self
.enumsuffix
)
1084 tick
.temp_fracenum
= "%s%s%s%i%s" % (self
.enumprefix
, fracenumminus
, self
.enuminfix
, tick
.temp_fracenum
, self
.enumsuffix
)
1085 if self
.skipdenom1
and tick
.temp_fracdenom
== 1 and not len(fracdenomminus
) and not len(self
.denomprefix
) and not len(self
.denominfix
) and not len(self
.denomsuffix
):
1086 frac
= tick
.temp_fracenum
1088 tick
.temp_fracdenom
= "%s%s%s%i%s" % (self
.denomprefix
, fracdenomminus
, self
.denominfix
, tick
.temp_fracdenom
, self
.denomsuffix
)
1089 frac
= self
.over
% (tick
.temp_fracenum
, tick
.temp_fracdenom
)
1090 tick
.label
= "%s%s%s%s%s" % (self
.prefix
, fracminus
, self
.infix
, frac
, self
.suffix
)
1091 tick
.labelattrs
= tick
.labelattrs
+ self
.labelattrs
1093 # del tick.temp_fracenum # we've inserted those temporary variables ... and do not care any longer about them
1094 # del tick.temp_fracdenom
1095 # del tick.temp_fracminus
1099 class decimaltexter
:
1100 "a texter creating decimal labels (e.g. '1.234' or even '0.\overline{3}')"
1102 __implements__
= _Itexter
1104 def __init__(self
, prefix
="", infix
="", suffix
="", equalprecision
=0,
1105 decimalsep
=".", thousandsep
="", thousandthpartsep
="",
1106 plus
="", minus
="-", period
=r
"\overline{%s}",
1107 labelattrs
=[textmodule
.mathmode
]):
1108 r
"""initializes the instance
1109 - prefix, infix, and suffix (strings) are added at the begin,
1110 immediately after the minus, and at the end of the label,
1112 - decimalsep, thousandsep, and thousandthpartsep (strings)
1113 are used as separators
1114 - plus or minus (string) is inserted for non-negative or negative numbers
1115 - period (string) is taken as a format string generating a period;
1116 it has to contain exactly one string insert operators "%s" for the
1117 period; usually it should be r"\overline{%s}"
1118 - labelattrs is a list of attributes for a texrunners text method;
1119 a single is allowed without being a list; None is considered as
1120 an empty list; labelattrs might be changed in the painter as well"""
1121 self
.prefix
= prefix
1123 self
.suffix
= suffix
1124 self
.equalprecision
= equalprecision
1125 self
.decimalsep
= decimalsep
1126 self
.thousandsep
= thousandsep
1127 self
.thousandthpartsep
= thousandthpartsep
1130 self
.period
= period
1131 self
.labelattrs
= labelattrs
1133 def labels(self
, ticks
):
1137 if tick
.label
is None and tick
.labellevel
is not None:
1138 labeledticks
.append(tick
)
1139 m
, n
= tick
.enum
, tick
.denom
1142 quotient
, remainder
= divmod(m
, n
)
1143 quotient
= str(quotient
)
1144 if len(self
.thousandsep
):
1148 tick
.label
+= quotient
[i
]
1149 if not ((l
-i
-1) % 3) and l
> i
+1:
1150 tick
.label
+= self
.thousandsep
1152 tick
.label
= quotient
1154 tick
.label
+= self
.decimalsep
1156 tick
.temp_decprecision
= 0
1158 tick
.temp_decprecision
+= 1
1159 if remainder
in oldremainders
:
1160 tick
.temp_decprecision
= None
1161 periodstart
= len(tick
.label
) - (len(oldremainders
) - oldremainders
.index(remainder
))
1162 tick
.label
= tick
.label
[:periodstart
] + self
.period
% tick
.label
[periodstart
:]
1164 oldremainders
+= [remainder
]
1166 quotient
, remainder
= divmod(remainder
, n
)
1167 if not ((tick
.temp_decprecision
- 1) % 3) and tick
.temp_decprecision
> 1:
1168 tick
.label
+= self
.thousandthpartsep
1169 tick
.label
+= str(quotient
)
1170 if maxdecprecision
< tick
.temp_decprecision
:
1171 maxdecprecision
= tick
.temp_decprecision
1172 if self
.equalprecision
:
1173 for tick
in labeledticks
:
1174 if tick
.temp_decprecision
is not None:
1175 if tick
.temp_decprecision
== 0 and maxdecprecision
> 0:
1176 tick
.label
+= self
.decimalsep
1177 for i
in range(tick
.temp_decprecision
, maxdecprecision
):
1178 if not ((i
- 1) % 3) and i
> 1:
1179 tick
.label
+= self
.thousandthpartsep
1181 for tick
in labeledticks
:
1182 if tick
.enum
* tick
.denom
< 0:
1183 plusminus
= self
.minus
1185 plusminus
= self
.plus
1186 tick
.label
= "%s%s%s%s%s" % (self
.prefix
, plusminus
, self
.infix
, tick
.label
, self
.suffix
)
1187 tick
.labelattrs
= tick
.labelattrs
+ self
.labelattrs
1189 # del tick.temp_decprecision # we've inserted this temporary variable ... and do not care any longer about it
1192 class exponentialtexter
:
1193 "a texter creating labels with exponentials (e.g. '2\cdot10^5')"
1195 __implements__
= _Itexter
1197 def __init__(self
, plus
="", minus
="-",
1198 mantissaexp
=r
"{{%s}\cdot10^{%s}}",
1201 nomantissaexp
=r
"{10^{%s}}",
1202 minusnomantissaexp
=r
"{-10^{%s}}",
1203 mantissamin
=frac((1, 1)), mantissamax
=frac((10, 1)),
1204 skipmantissa1
=0, skipallmantissa1
=1,
1205 mantissatexter
=decimaltexter()):
1206 r
"""initializes the instance
1207 - plus or minus (string) is inserted for non-negative or negative exponents
1208 - mantissaexp (string) is taken as a format string generating the exponent;
1209 it has to contain exactly two string insert operators "%s" --
1210 the first for the mantissa and the second for the exponent;
1211 examples are r"{{%s}\cdot10^{%s}}" and r"{{%s}{\rm e}{%s}}"
1212 - skipexp0 (string) is taken as a format string used for exponent 0;
1213 exactly one string insert operators "%s" for the mantissa;
1214 None turns off the special handling of exponent 0;
1215 an example is r"{%s}"
1216 - skipexp1 (string) is taken as a format string used for exponent 1;
1217 exactly one string insert operators "%s" for the mantissa;
1218 None turns off the special handling of exponent 1;
1219 an example is r"{{%s}\cdot10}"
1220 - nomantissaexp (string) is taken as a format string generating the exponent
1221 when the mantissa is one and should be skipped; it has to contain
1222 exactly one string insert operators "%s" for the exponent;
1223 an examples is r"{10^{%s}}"
1224 - minusnomantissaexp (string) is taken as a format string generating the exponent
1225 when the mantissa is minus one and should be skipped; it has to contain
1226 exactly one string insert operators "%s" for the exponent;
1227 None turns off the special handling of mantissa -1;
1228 an examples is r"{-10^{%s}}"
1229 - mantissamin and mantissamax are the minimum and maximum of the mantissa;
1230 they are frac instances greater than zero and mantissamin < mantissamax;
1231 the sign of the tick is ignored here
1232 - skipmantissa1 (boolean) turns on skipping of any mantissa equals one
1233 (and minus when minusnomantissaexp is set)
1234 - skipallmantissa1 (boolean) as above, but all mantissas must be 1 (or -1)
1235 - mantissatexter is the texter for the mantissa
1236 - the skipping of a mantissa is stronger than the skipping of an exponent"""
1239 self
.mantissaexp
= mantissaexp
1240 self
.skipexp0
= skipexp0
1241 self
.skipexp1
= skipexp1
1242 self
.nomantissaexp
= nomantissaexp
1243 self
.minusnomantissaexp
= minusnomantissaexp
1244 self
.mantissamin
= mantissamin
1245 self
.mantissamax
= mantissamax
1246 self
.mantissamindivmax
= self
.mantissamin
/ self
.mantissamax
1247 self
.mantissamaxdivmin
= self
.mantissamax
/ self
.mantissamin
1248 self
.skipmantissa1
= skipmantissa1
1249 self
.skipallmantissa1
= skipallmantissa1
1250 self
.mantissatexter
= mantissatexter
1252 def labels(self
, ticks
):
1255 if tick
.label
is None and tick
.labellevel
is not None:
1256 tick
.temp_orgenum
, tick
.temp_orgdenom
= tick
.enum
, tick
.denom
1257 labeledticks
.append(tick
)
1260 while abs(tick
) >= self
.mantissamax
:
1262 x
= tick
* self
.mantissamindivmax
1263 tick
.enum
, tick
.denom
= x
.enum
, x
.denom
1264 while abs(tick
) < self
.mantissamin
:
1266 x
= tick
* self
.mantissamaxdivmin
1267 tick
.enum
, tick
.denom
= x
.enum
, x
.denom
1268 if tick
.temp_exp
< 0:
1269 tick
.temp_exp
= "%s%i" % (self
.minus
, -tick
.temp_exp
)
1271 tick
.temp_exp
= "%s%i" % (self
.plus
, tick
.temp_exp
)
1272 self
.mantissatexter
.labels(labeledticks
)
1273 if self
.minusnomantissaexp
is not None:
1274 allmantissa1
= len(labeledticks
) == len([tick
for tick
in labeledticks
if abs(tick
.enum
) == abs(tick
.denom
)])
1276 allmantissa1
= len(labeledticks
) == len([tick
for tick
in labeledticks
if tick
.enum
== tick
.denom
])
1277 for tick
in labeledticks
:
1278 if (self
.skipallmantissa1
and allmantissa1
or
1279 (self
.skipmantissa1
and (tick
.enum
== tick
.denom
or
1280 (tick
.enum
== -tick
.denom
and self
.minusnomantissaexp
is not None)))):
1281 if tick
.enum
== tick
.denom
:
1282 tick
.label
= self
.nomantissaexp
% tick
.temp_exp
1284 tick
.label
= self
.minusnomantissaexp
% tick
.temp_exp
1286 if tick
.temp_exp
== "0" and self
.skipexp0
is not None:
1287 tick
.label
= self
.skipexp0
% tick
.label
1288 elif tick
.temp_exp
== "1" and self
.skipexp1
is not None:
1289 tick
.label
= self
.skipexp1
% tick
.label
1291 tick
.label
= self
.mantissaexp
% (tick
.label
, tick
.temp_exp
)
1292 tick
.enum
, tick
.denom
= tick
.temp_orgenum
, tick
.temp_orgdenom
1294 # del tick.temp_orgenum # we've inserted those temporary variables ... and do not care any longer about them
1295 # del tick.temp_orgdenom
1299 class defaulttexter
:
1300 "a texter creating decimal or exponential labels"
1302 __implements__
= _Itexter
1304 def __init__(self
, smallestdecimal
=frac((1, 1000)),
1305 biggestdecimal
=frac((9999, 1)),
1307 decimaltexter
=decimaltexter(),
1308 exponentialtexter
=exponentialtexter()):
1309 """initializes the instance
1310 - smallestdecimal and biggestdecimal are the smallest and
1311 biggest decimal values, where the decimaltexter should be used;
1312 they are frac instances; the sign of the tick is ignored here;
1313 a tick at zero is considered for the decimaltexter as well
1314 - equaldecision (boolean) uses decimaltexter or exponentialtexter
1315 globaly (set) or for each tick separately (unset)
1316 - decimaltexter and exponentialtexter are texters to be used"""
1317 self
.smallestdecimal
= smallestdecimal
1318 self
.biggestdecimal
= biggestdecimal
1319 self
.equaldecision
= equaldecision
1320 self
.decimaltexter
= decimaltexter
1321 self
.exponentialtexter
= exponentialtexter
1323 def labels(self
, ticks
):
1327 if tick
.label
is None and tick
.labellevel
is not None:
1328 if not tick
.enum
or (abs(tick
) >= self
.smallestdecimal
and abs(tick
) <= self
.biggestdecimal
):
1329 decticks
.append(tick
)
1331 expticks
.append(tick
)
1332 if self
.equaldecision
:
1334 self
.exponentialtexter
.labels(ticks
)
1336 self
.decimaltexter
.labels(ticks
)
1338 for tick
in decticks
:
1339 self
.decimaltexter
.labels([tick
])
1340 for tick
in expticks
:
1341 self
.exponentialtexter
.labels([tick
])
1344 ################################################################################
1346 ################################################################################
1349 class axiscanvas(canvas
._canvas
):
1351 - an axis canvas is a regular canvas returned by an
1352 axispainters painter method
1353 - it contains a PyX length extent to be used for the
1354 alignment of additional axes; the axis extent should
1355 be handled by the axispainters painter method; you may
1356 apprehend this as a size information comparable to a
1357 bounding box, which must be handled manually
1358 - it contains a list of textboxes called labels which are
1359 used to rate the distances between the labels if needed
1360 by the axis later on; the painter method has not only to
1361 insert the labels into this canvas, but should also fill
1362 this list, when a rating of the distances should be
1363 performed by the axis"""
1365 # __implements__ = sole implementation
1367 def __init__(self
, *args
, **kwargs
):
1368 """initializes the instance
1369 - sets extent to zero
1370 - sets labels to an empty list"""
1371 canvas
._canvas
.__init
__(self
, *args
, **kwargs
)
1377 """create rotations accordingly to tick directions
1378 - upsidedown rotations are suppressed by rotating them by another 180 degree"""
1380 # __implements__ = sole implementation
1382 def __init__(self
, direction
, epsilon
=1e-10):
1383 """initializes the instance
1384 - direction is an angle to be used relative to the tick direction
1385 - epsilon is the value by which 90 degrees can be exceeded before
1386 an 180 degree rotation is added"""
1387 self
.direction
= direction
1388 self
.epsilon
= epsilon
1390 def trafo(self
, dx
, dy
):
1391 """returns a rotation transformation accordingly to the tick direction
1392 - dx and dy are the direction of the tick"""
1393 direction
= self
.direction
+ math
.atan2(dy
, dx
) * 180 / math
.pi
1394 while (direction
> 180 + self
.epsilon
):
1396 while (direction
< -180 - self
.epsilon
):
1398 while (direction
> 90 + self
.epsilon
):
1400 while (direction
< -90 - self
.epsilon
):
1402 return trafomodule
.rotate(direction
)
1405 rotatetext
.parallel
= rotatetext(90)
1406 rotatetext
.orthogonal
= rotatetext(180)
1409 class _Iaxispainter
:
1410 "class for painting axes"
1412 def paint(self
, axispos
, axis
, ac
=None):
1413 """paint the axis into an axiscanvas
1414 - returns the axiscanvas
1415 - when no axiscanvas is provided (the typical case), a new
1416 axiscanvas is created. however, when extending an painter
1417 by inheritance, painting on the same axiscanvas is supported
1418 by setting the axiscanvas attribute
1419 - axispos is an instance, which implements _Iaxispos to
1420 define the tick positions
1421 - the axis and should not be modified (we may
1422 add some temporary variables like axis.ticks[i].temp_xxx,
1423 which might be used just temporary) -- the idea is that
1424 all things can be used several times
1425 - also do not modify the instance (self) -- even this
1426 instance might be used several times; thus do not modify
1427 attributes like self.titleattrs etc. (use local copies)
1428 - the method might access some additional attributes from
1429 the axis, e.g. the axis title -- the axis painter should
1430 document this behavior and rely on the availability of
1431 those attributes -> it becomes a question of the proper
1432 usage of the combination of axis & axispainter
1433 - the axiscanvas is a axiscanvas instance and should be
1434 filled with ticks, labels, title, etc.; note that the
1435 extent and labels instance variables should be handled
1436 as documented in the axiscanvas"""
1440 """interface definition of axis tick position methods
1441 - these methods are used for the postitioning of the ticks
1442 when painting an axis"""
1443 # TODO: should we add a local transformation (for label text etc?)
1444 # (this might replace tickdirection (and even tickposition?))
1446 def basepath(self
, x1
=None, x2
=None):
1447 """return the basepath as a path
1448 - x1 is the start position; if not set, the basepath starts
1449 from the beginning of the axis, which might imply a
1450 value outside of the graph coordinate range [0; 1]
1451 - x2 is analogous to x1, but for the end position"""
1453 def vbasepath(self
, v1
=None, v2
=None):
1454 """return the basepath as a path
1455 - like basepath, but for graph coordinates"""
1457 def gridpath(self
, x
):
1458 """return the gridpath as a path for a given position x
1459 - might return None when no gridpath is available"""
1461 def vgridpath(self
, v
):
1462 """return the gridpath as a path for a given position v
1463 in graph coordinates
1464 - might return None when no gridpath is available"""
1466 def tickpoint_pt(self
, x
):
1467 """return the position at the basepath as a tuple (x, y) in
1468 postscript points for the position x"""
1470 def tickpoint(self
, x
):
1471 """return the position at the basepath as a tuple (x, y) in
1472 in PyX length for the position x"""
1474 def vtickpoint_pt(self
, v
):
1475 "like tickpoint_pt, but for graph coordinates"
1477 def vtickpoint(self
, v
):
1478 "like tickpoint, but for graph coordinates"
1480 def tickdirection(self
, x
):
1481 """return the direction of a tick as a tuple (dx, dy) for the
1482 position x (the direction points towards the graph)"""
1484 def vtickdirection(self
, v
):
1485 """like tickposition, but for graph coordinates"""
1489 """implements those parts of _Iaxispos which can be build
1490 out of the axis convert method and other _Iaxispos methods
1491 - base _Iaxispos methods, which need to be implemented:
1496 - other methods needed for _Iaxispos are build out of those
1497 listed above when this class is inherited"""
1499 def __init__(self
, convert
):
1500 """initializes the instance
1501 - convert is a convert method from an axis"""
1502 self
.convert
= convert
1504 def basepath(self
, x1
=None, x2
=None):
1507 return self
.vbasepath()
1509 return self
.vbasepath(v2
=self
.convert(x2
))
1512 return self
.vbasepath(v1
=self
.convert(x1
))
1514 return self
.vbasepath(v1
=self
.convert(x1
), v2
=self
.convert(x2
))
1516 def gridpath(self
, x
):
1517 return self
.vgridpath(self
.convert(x
))
1519 def tickpoint_pt(self
, x
):
1520 return self
.vtickpoint_pt(self
.convert(x
))
1522 def tickpoint(self
, x
):
1523 return self
.vtickpoint(self
.convert(x
))
1525 def vtickpoint(self
, v
):
1526 return [unit
.t_pt(x
) for x
in self
.vtickpoint(v
)]
1528 def tickdirection(self
, x
):
1529 return self
.vtickdirection(self
.convert(x
))
1532 class pathaxispos(_axispos
):
1533 """axis tick position methods along an arbitrary path"""
1535 __implements__
= _Iaxispos
1537 def __init__(self
, p
, convert
, direction
=1):
1539 self
.normpath
= path
.normpath(p
)
1540 self
.arclength
= self
.normpath
.arclength()
1541 _axispos
.__init
__(self
, convert
)
1542 self
.direction
= direction
1544 def vbasepath(self
, v1
=None, v2
=None):
1549 return self
.normpath
.split(self
.normpath
.lentopar(v2
* self
.arclength
))[0]
1552 return self
.normpath
.split(self
.normpath
.lentopar(v1
* self
.arclength
))[1]
1554 return self
.normpath
.split(*self
.normpath
.lentopar([v1
* self
.arclength
, v2
* self
.arclength
]))[1]
1556 def vgridpath(self
, v
):
1559 def vtickpoint_pt(self
, v
):
1560 # XXX: path._at missing!
1561 return [unit
.topt(x
) for x
in self
.normpath
.at(self
.normpath
.lentopar(v
* self
.arclength
))]
1563 def vtickdirection(self
, v
):
1564 t
= self
.normpath
.tangent(self
.normpath
.lentopar(v
* self
.arclength
))
1565 # XXX: path._begin and path._end missing!
1566 tbegin
= [unit
.topt(x
) for x
in t
.begin()]
1567 tend
= [unit
.topt(x
) for x
in t
.end()]
1568 dx
= tend
[0]-tbegin
[0]
1569 dy
= tend
[1]-tbegin
[1]
1570 norm
= math
.sqrt(dx
*dx
+ dy
*dy
)
1571 if self
.direction
== 1:
1572 return -dy
/norm
, dx
/norm
1573 elif self
.direction
== -1:
1574 return dy
/norm
, -dx
/norm
1575 raise RuntimeError("unknown direction")
1578 class axistitlepainter
:
1579 """class for painting an axis title
1580 - the axis must have a title attribute when using this painter;
1581 this title might be None"""
1583 __implements__
= _Iaxispainter
1585 defaulttitleattrs
= [textmodule
.halign
.center
, textmodule
.vshift
.mathaxis
]
1587 def __init__(self
, titledist
="0.3 cm",
1589 titledirection
=rotatetext
.parallel
,
1591 texrunner
=textmodule
.defaulttexrunner
):
1592 """initialized the instance
1593 - titledist is a visual PyX length giving the distance
1594 of the title from the axis extent already there (a title might
1595 be added after labels or other things are plotted already)
1596 - titleattrs is a list of attributes for a texrunners text
1597 method; a single is allowed without being a list; None
1599 - titledirection is an instance of rotatetext or None
1600 - titlepos is the position of the title in graph coordinates
1601 - texrunner is the texrunner to be used to create text
1602 (the texrunner is available for further use in derived
1603 classes as instance variable texrunner)"""
1604 self
.titledist_str
= titledist
1605 self
.titleattrs
= titleattrs
1606 self
.titledirection
= titledirection
1607 self
.titlepos
= titlepos
1608 self
.texrunner
= texrunner
1610 def paint(self
, axispos
, axis
, ac
=None):
1613 if axis
.title
is not None and self
.titleattrs
is not None:
1614 titledist
= unit
.length(self
.titledist_str
, default_type
="v")
1615 x
, y
= axispos
.vtickpoint_pt(self
.titlepos
)
1616 dx
, dy
= axispos
.vtickdirection(self
.titlepos
)
1617 titleattrs
= self
.defaulttitleattrs
+ self
.titleattrs
1618 if self
.titledirection
is not None:
1619 titleattrs
.append(self
.titledirection
.trafo(dx
, dy
))
1620 title
= self
.texrunner
.text_pt(x
, y
, axis
.title
, titleattrs
)
1621 ac
.extent
+= titledist
1622 title
.linealign(ac
.extent
, -dx
, -dy
)
1623 ac
.extent
+= title
.extent(dx
, dy
)
1628 class geometricseries(attr
.changeattr
):
1630 def __init__(self
, initial
, factor
):
1631 self
.initial
= initial
1632 self
.factor
= factor
1634 def select(self
, index
, total
):
1635 return self
.initial
* (self
.factor
** index
)
1638 class ticklength(geometricseries
): pass
1642 #ticklength.short = ticklength("%f cm" % (_base/math.sqrt(64)), 1/goldenmean)
1643 ticklength
.short
= ticklength(_base
/math
.sqrt(64), 1/goldenmean
)
1644 ticklength
.short
= ticklength(_base
/math
.sqrt(32), 1/goldenmean
)
1645 ticklength
.short
= ticklength(_base
/math
.sqrt(16), 1/goldenmean
)
1646 ticklength
.short
= ticklength(_base
/math
.sqrt(8), 1/goldenmean
)
1647 ticklength
.short
= ticklength(_base
/math
.sqrt(4), 1/goldenmean
)
1648 ticklength
.short
= ticklength(_base
/math
.sqrt(2), 1/goldenmean
)
1649 ticklength
.normal
= ticklength(_base
, 1/goldenmean
)
1650 ticklength
.long = ticklength(_base
*math
.sqrt(2), 1/goldenmean
)
1651 ticklength
.long = ticklength(_base
*math
.sqrt(4), 1/goldenmean
)
1652 ticklength
.long = ticklength(_base
*math
.sqrt(8), 1/goldenmean
)
1653 ticklength
.long = ticklength(_base
*math
.sqrt(16), 1/goldenmean
)
1654 ticklength
.long = ticklength(_base
*math
.sqrt(32), 1/goldenmean
)
1657 class axispainter(axistitlepainter
):
1658 """class for painting the ticks and labels of an axis
1659 - the inherited titleaxispainter is used to paint the title of
1661 - note that the type of the elements of ticks given as an argument
1662 of the paint method must be suitable for the tick position methods
1665 __implements__
= _Iaxispainter
1667 defaulttickattrs
= []
1668 defaultgridattrs
= []
1669 defaultbasepathattrs
= [style
.linecap
.square
]
1670 defaultlabelattrs
= [textmodule
.halign
.center
, textmodule
.vshift
.mathaxis
]
1672 def __init__(self
, innerticklength
=ticklength
.short
,
1673 outerticklength
=None,
1679 labeldirection
=None,
1683 """initializes the instance
1684 - innerticklength and outerticklength are two lists of
1685 visual PyX lengths for ticks, subticks, etc. plotted inside
1686 and outside of the graph; when a single value is given, it
1687 is used for all tick levels; None turns off ticks inside or
1688 outside of the graph
1689 - tickattrs are a list of stroke attributes for the ticks;
1690 a single entry is allowed without being a list; None turns
1692 - gridattrs are a list of lists used as stroke
1693 attributes for ticks, subticks etc.; when a single list
1694 is given, it is used for ticks, subticks, etc.; a single
1695 entry is allowed without being a list; None turns off
1697 - basepathattrs are a list of stroke attributes for a grid
1698 line at axis value zero; a single entry is allowed without
1699 being a list; None turns off the basepath
1700 - labeldist is a visual PyX length for the distance of the labels
1701 from the axis basepath
1702 - labelattrs is a list of attributes for a texrunners text
1703 method; a single entry is allowed without being a list;
1704 None turns off the labels
1705 - titledirection is an instance of rotatetext or None
1706 - labelhequalize and labelvequalize (booleans) perform an equal
1707 alignment for straight vertical and horizontal axes, respectively
1708 - futher keyword arguments are passed to axistitlepainter"""
1709 # TODO: access to axis.divisor -- document, remove, ... ???
1710 self
.innerticklength_str
= innerticklength
1711 self
.outerticklength_str
= outerticklength
1712 self
.tickattrs
= tickattrs
1713 self
.gridattrs
= gridattrs
1714 self
.basepathattrs
= basepathattrs
1715 self
.labeldist_str
= labeldist
1716 self
.labelattrs
= labelattrs
1717 self
.labeldirection
= labeldirection
1718 self
.labelhequalize
= labelhequalize
1719 self
.labelvequalize
= labelvequalize
1720 axistitlepainter
.__init
__(self
, **kwargs
)
1722 def paint(self
, axispos
, axis
, ac
=None):
1725 labeldist
= unit
.length(self
.labeldist_str
, default_type
="v")
1726 for tick
in axis
.ticks
:
1727 tick
.temp_v
= axis
.convert(float(tick
) * axis
.divisor
)
1728 tick
.temp_x
, tick
.temp_y
= axispos
.vtickpoint_pt(tick
.temp_v
)
1729 tick
.temp_dx
, tick
.temp_dy
= axispos
.vtickdirection(tick
.temp_v
)
1730 maxticklevel
, maxlabellevel
= _maxlevels(axis
.ticks
)
1732 # create & align tick.temp_labelbox
1733 for tick
in axis
.ticks
:
1734 if tick
.labellevel
is not None:
1735 labelattrs
= attr
.selectattrs(self
.labelattrs
, tick
.labellevel
, maxlabellevel
)
1736 if labelattrs
is not None:
1737 labelattrs
= self
.defaultlabelattrs
+ labelattrs
1738 if self
.labeldirection
is not None:
1739 labelattrs
.append(self
.labeldirection
.trafo(tick
.temp_dx
, tick
.temp_dy
))
1740 if tick
.labelattrs
is not None:
1741 labelattrs
.extend(tick
.labelattrs
)
1742 tick
.temp_labelbox
= self
.texrunner
.text_pt(tick
.temp_x
, tick
.temp_y
, tick
.label
, labelattrs
)
1743 if len(axis
.ticks
) > 1:
1745 for tick
in axis
.ticks
[1:]:
1746 if tick
.temp_dx
!= axis
.ticks
[0].temp_dx
or tick
.temp_dy
!= axis
.ticks
[0].temp_dy
:
1750 if equaldirection
and ((not axis
.ticks
[0].temp_dx
and self
.labelvequalize
) or
1751 (not axis
.ticks
[0].temp_dy
and self
.labelhequalize
)):
1752 if self
.labelattrs
is not None:
1753 box
.linealignequal([tick
.temp_labelbox
for tick
in axis
.ticks
if tick
.labellevel
is not None],
1754 labeldist
, -axis
.ticks
[0].temp_dx
, -axis
.ticks
[0].temp_dy
)
1756 for tick
in axis
.ticks
:
1757 if tick
.labellevel
is not None and self
.labelattrs
is not None:
1758 tick
.temp_labelbox
.linealign(labeldist
, -tick
.temp_dx
, -tick
.temp_dy
)
1760 for tick
in axis
.ticks
:
1761 if tick
.ticklevel
is not None:
1762 innerticklength
= attr
.selectattr(self
.innerticklength_str
, tick
.ticklevel
, maxticklevel
)
1763 outerticklength
= attr
.selectattr(self
.outerticklength_str
, tick
.ticklevel
, maxticklevel
)
1764 if innerticklength
is not None or outerticklength
is not None:
1765 if innerticklength
is None:
1768 innerticklength
= unit
.length(innerticklength
, default_type
="v")
1769 if outerticklength
is None:
1772 outerticklength
= unit
.length(outerticklength
, default_type
="v")
1773 tickattrs
= attr
.selectattrs(self
.defaulttickattrs
+ self
.tickattrs
, tick
.ticklevel
, maxticklevel
)
1774 if tickattrs
is not None:
1775 innerticklength_pt
= unit
.topt(innerticklength
)
1776 outerticklength_pt
= unit
.topt(outerticklength
)
1777 x1
= tick
.temp_x
+ tick
.temp_dx
* innerticklength_pt
1778 y1
= tick
.temp_y
+ tick
.temp_dy
* innerticklength_pt
1779 x2
= tick
.temp_x
- tick
.temp_dx
* outerticklength_pt
1780 y2
= tick
.temp_y
- tick
.temp_dy
* outerticklength_pt
1781 ac
.stroke(path
.line_pt(x1
, y1
, x2
, y2
), tickattrs
)
1782 if outerticklength
is not None and unit
.topt(outerticklength
) > unit
.topt(ac
.extent
):
1783 ac
.extent
= outerticklength
1784 if outerticklength
is not None and unit
.topt(-innerticklength
) > unit
.topt(ac
.extent
):
1785 ac
.extent
= -innerticklength
1786 if self
.gridattrs
is not None:
1787 gridattrs
= attr
.selectattrs(self
.defaultgridattrs
+ self
.gridattrs
, tick
.ticklevel
, maxticklevel
)
1788 ac
.stroke(axispos
.vgridpath(tick
.temp_v
), gridattrs
)
1789 if tick
.labellevel
is not None and self
.labelattrs
is not None:
1790 ac
.insert(tick
.temp_labelbox
)
1791 ac
.labels
.append(tick
.temp_labelbox
)
1792 extent
= tick
.temp_labelbox
.extent(tick
.temp_dx
, tick
.temp_dy
) + labeldist
1793 if unit
.topt(extent
) > unit
.topt(ac
.extent
):
1795 if self
.basepathattrs
is not None:
1796 ac
.stroke(axispos
.vbasepath(), self
.defaultbasepathattrs
+ self
.basepathattrs
)
1798 # for tick in axis.ticks:
1799 # del tick.temp_v # we've inserted those temporary variables ... and do not care any longer about them
1804 # if tick.labellevel is not None and self.labelattrs is not None:
1805 # del tick.temp_labelbox
1807 axistitlepainter
.paint(self
, axispos
, axis
, ac
=ac
)
1812 class linkaxispainter(axispainter
):
1813 """class for painting a linked axis
1814 - the inherited axispainter is used to paint the axis
1815 - modifies some constructor defaults"""
1817 __implements__
= _Iaxispainter
1819 def __init__(self
, labelattrs
=None,
1822 """initializes the instance
1823 - the labelattrs default is set to None thus skipping the labels
1824 - the titleattrs default is set to None thus skipping the title
1825 - all keyword arguments are passed to axispainter"""
1826 axispainter
.__init
__(self
, labelattrs
=labelattrs
,
1827 titleattrs
=titleattrs
,
1832 """implementation of the _Iaxispos interface for a subaxis"""
1834 __implements__
= _Iaxispos
1836 def __init__(self
, convert
, baseaxispos
, vmin
, vmax
, vminover
, vmaxover
):
1837 """initializes the instance
1838 - convert is the subaxis convert method
1839 - baseaxispos is the axispos instance of the base axis
1840 - vmin, vmax is the range covered by the subaxis in graph coordinates
1841 - vminover, vmaxover is the extended range of the subaxis including
1842 regions between several subaxes (for basepath drawing etc.)"""
1843 self
.convert
= convert
1844 self
.baseaxispos
= baseaxispos
1847 self
.vminover
= vminover
1848 self
.vmaxover
= vmaxover
1850 def basepath(self
, x1
=None, x2
=None):
1852 v1
= self
.vmin
+self
.convert(x1
)*(self
.vmax
-self
.vmin
)
1856 v2
= self
.vmin
+self
.convert(x2
)*(self
.vmax
-self
.vmin
)
1859 return self
.baseaxispos
.vbasepath(v1
, v2
)
1861 def vbasepath(self
, v1
=None, v2
=None):
1863 v1
= self
.vmin
+v1
*(self
.vmax
-self
.vmin
)
1867 v2
= self
.vmin
+v2
*(self
.vmax
-self
.vmin
)
1870 return self
.baseaxispos
.vbasepath(v1
, v2
)
1872 def gridpath(self
, x
):
1873 return self
.baseaxispos
.vgridpath(self
.vmin
+self
.convert(x
)*(self
.vmax
-self
.vmin
))
1875 def vgridpath(self
, v
):
1876 return self
.baseaxispos
.vgridpath(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
1878 def tickpoint_pt(self
, x
, axis
=None):
1879 return self
.baseaxispos
.vtickpoint_pt(self
.vmin
+self
.convert(x
)*(self
.vmax
-self
.vmin
))
1881 def tickpoint(self
, x
, axis
=None):
1882 return self
.baseaxispos
.vtickpoint(self
.vmin
+self
.convert(x
)*(self
.vmax
-self
.vmin
))
1884 def vtickpoint_pt(self
, v
, axis
=None):
1885 return self
.baseaxispos
.vtickpoint_pt(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
1887 def vtickpoint(self
, v
, axis
=None):
1888 return self
.baseaxispos
.vtickpoint(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
1890 def tickdirection(self
, x
, axis
=None):
1891 return self
.baseaxispos
.vtickdirection(self
.vmin
+self
.convert(x
)*(self
.vmax
-self
.vmin
))
1893 def vtickdirection(self
, v
, axis
=None):
1894 return self
.baseaxispos
.vtickdirection(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
1897 class splitaxispainter(axistitlepainter
):
1898 """class for painting a splitaxis
1899 - the inherited titleaxispainter is used to paint the title of
1901 - the splitaxispainter access the subaxes attribute of the axis"""
1903 __implements__
= _Iaxispainter
1905 defaultbreaklinesattrs
= []
1907 def __init__(self
, breaklinesdist
="0.05 cm",
1908 breaklineslength
="0.5 cm",
1909 breaklinesangle
=-60,
1912 """initializes the instance
1913 - breaklinesdist is a visual length of the distance between
1914 the two lines of the axis break
1915 - breaklineslength is a visual length of the length of the
1916 two lines of the axis break
1917 - breaklinesangle is the angle of the lines of the axis break
1918 - breaklinesattrs are a list of stroke attributes for the
1919 axis break lines; a single entry is allowed without being a
1920 list; None turns off the break lines
1921 - futher keyword arguments are passed to axistitlepainter"""
1922 self
.breaklinesdist_str
= breaklinesdist
1923 self
.breaklineslength_str
= breaklineslength
1924 self
.breaklinesangle
= breaklinesangle
1925 self
.breaklinesattrs
= breaklinesattrs
1926 axistitlepainter
.__init
__(self
, **args
)
1928 def paint(self
, axispos
, axis
, ac
=None):
1931 for subaxis
in axis
.subaxes
:
1932 subaxis
.finish(subaxispos(subaxis
.convert
, axispos
, subaxis
.vmin
, subaxis
.vmax
, subaxis
.vminover
, subaxis
.vmaxover
))
1933 ac
.insert(subaxis
.axiscanvas
)
1934 if unit
.topt(ac
.extent
) < unit
.topt(subaxis
.axiscanvas
.extent
):
1935 ac
.extent
= subaxis
.axiscanvas
.extent
1936 if self
.breaklinesattrs
is not None:
1937 self
.breaklinesdist
= unit
.length(self
.breaklinesdist_str
, default_type
="v")
1938 self
.breaklineslength
= unit
.length(self
.breaklineslength_str
, default_type
="v")
1939 self
.sin
= math
.sin(self
.breaklinesangle
*math
.pi
/180.0)
1940 self
.cos
= math
.cos(self
.breaklinesangle
*math
.pi
/180.0)
1941 breaklinesextent
= (0.5*self
.breaklinesdist
*math
.fabs(self
.cos
) +
1942 0.5*self
.breaklineslength
*math
.fabs(self
.sin
))
1943 if unit
.topt(ac
.extent
) < unit
.topt(breaklinesextent
):
1944 ac
.extent
= breaklinesextent
1945 for subaxis1
, subaxis2
in zip(axis
.subaxes
[:-1], axis
.subaxes
[1:]):
1946 # use a tangent of the basepath (this is independent of the tickdirection)
1947 v
= 0.5 * (subaxis1
.vmax
+ subaxis2
.vmin
)
1948 p
= path
.normpath(axispos
.vbasepath(v
, None))
1949 breakline
= p
.tangent(0, self
.breaklineslength
)
1950 widthline
= p
.tangent(0, self
.breaklinesdist
).transformed(trafomodule
.rotate(self
.breaklinesangle
+90, *breakline
.begin()))
1951 tocenter
= map(lambda x
: 0.5*(x
[0]-x
[1]), zip(breakline
.begin(), breakline
.end()))
1952 towidth
= map(lambda x
: 0.5*(x
[0]-x
[1]), zip(widthline
.begin(), widthline
.end()))
1953 breakline
= breakline
.transformed(trafomodule
.translate(*tocenter
).rotated(self
.breaklinesangle
, *breakline
.begin()))
1954 breakline1
= breakline
.transformed(trafomodule
.translate(*towidth
))
1955 breakline2
= breakline
.transformed(trafomodule
.translate(-towidth
[0], -towidth
[1]))
1956 ac
.fill(path
.path(path
.moveto(*breakline1
.begin()),
1957 path
.lineto(*breakline1
.end()),
1958 path
.lineto(*breakline2
.end()),
1959 path
.lineto(*breakline2
.begin()),
1960 path
.closepath()), [color
.gray
.white
])
1961 ac
.stroke(breakline1
, self
.defaultbreaklinesattrs
+ self
.breaklinesattrs
)
1962 ac
.stroke(breakline2
, self
.defaultbreaklinesattrs
+ self
.breaklinesattrs
)
1963 axistitlepainter
.paint(self
, axispos
, axis
, ac
=ac
)
1967 class linksplitaxispainter(splitaxispainter
):
1968 """class for painting a linked splitaxis
1969 - the inherited splitaxispainter is used to paint the axis
1970 - modifies some constructor defaults"""
1972 __implements__
= _Iaxispainter
1974 def __init__(self
, titleattrs
=None, **kwargs
):
1975 """initializes the instance
1976 - the titleattrs default is set to None thus skipping the title
1977 - all keyword arguments are passed to splitaxispainter"""
1978 splitaxispainter
.__init
__(self
, titleattrs
=titleattrs
, **kwargs
)
1981 class baraxispainter(axistitlepainter
):
1982 """class for painting a baraxis
1983 - the inherited titleaxispainter is used to paint the title of
1985 - the baraxispainter access the multisubaxis, subaxis names, texts, and
1986 relsizes attributes"""
1988 __implements__
= _Iaxispainter
1990 defaulttickattrs
= []
1991 defaultbasepathattrs
= [style
.linecap
.square
]
1992 defaultnameattrs
= [textmodule
.halign
.center
, textmodule
.vshift
.mathaxis
]
1994 def __init__(self
, innerticklength
=None,
1995 outerticklength
=None,
2005 """initializes the instance
2006 - innerticklength and outerticklength are a visual length of
2007 the ticks to be plotted at the axis basepath to visually
2008 separate the bars; if neither innerticklength nor
2009 outerticklength are set, not ticks are plotted
2010 - breaklinesattrs are a list of stroke attributes for the
2011 axis tick; a single entry is allowed without being a
2012 list; None turns off the ticks
2013 - namedist is a visual PyX length for the distance of the bar
2014 names from the axis basepath
2015 - nameattrs is a list of attributes for a texrunners text
2016 method; a single entry is allowed without being a list;
2017 None turns off the names
2018 - namedirection is an instance of rotatetext or None
2019 - namehequalize and namevequalize (booleans) perform an equal
2020 alignment for straight vertical and horizontal axes, respectively
2021 - futher keyword arguments are passed to axistitlepainter"""
2022 self
.innerticklength_str
= innerticklength
2023 self
.outerticklength_str
= outerticklength
2024 self
.tickattrs
= tickattrs
2025 self
.basepathattrs
= basepathattrs
2026 self
.namedist_str
= namedist
2027 self
.nameattrs
= nameattrs
2028 self
.namedirection
= namedirection
2029 self
.namepos
= namepos
2030 self
.namehequalize
= namehequalize
2031 self
.namevequalize
= namevequalize
2032 axistitlepainter
.__init
__(self
, **args
)
2034 def paint(self
, axispos
, axis
, ac
=None):
2037 if axis
.multisubaxis
is not None:
2038 for subaxis
in axis
.subaxis
:
2039 subaxis
.finish(subaxispos(subaxis
.convert
, axispos
, subaxis
.vmin
, subaxis
.vmax
, None, None))
2040 ac
.insert(subaxis
.axiscanvas
)
2041 if unit
.topt(ac
.extent
) < unit
.topt(subaxis
.axiscanvas
.extent
):
2042 ac
.extent
= subaxis
.axiscanvas
.extent
2044 for name
in axis
.names
:
2045 v
= axis
.convert((name
, self
.namepos
))
2046 x
, y
= axispos
.vtickpoint_pt(v
)
2047 dx
, dy
= axispos
.vtickdirection(v
)
2048 namepos
.append((v
, x
, y
, dx
, dy
))
2050 if self
.nameattrs
is not None:
2051 for (v
, x
, y
, dx
, dy
), name
in zip(namepos
, axis
.names
):
2052 nameattrs
= self
.defaultnameattrs
+ self
.nameattrs
2053 if self
.namedirection
is not None:
2054 nameattrs
.append(self
.namedirection
.trafo(tick
.temp_dx
, tick
.temp_dy
))
2055 if axis
.texts
.has_key(name
):
2056 nameboxes
.append(self
.texrunner
.text_pt(x
, y
, str(axis
.texts
[name
]), nameattrs
))
2057 elif axis
.texts
.has_key(str(name
)):
2058 nameboxes
.append(self
.texrunner
.text_pt(x
, y
, str(axis
.texts
[str(name
)]), nameattrs
))
2060 nameboxes
.append(self
.texrunner
.text_pt(x
, y
, str(name
), nameattrs
))
2061 labeldist
= ac
.extent
+ unit
.length(self
.namedist_str
, default_type
="v")
2062 if len(namepos
) > 1:
2064 for np
in namepos
[1:]:
2065 if np
[3] != namepos
[0][3] or np
[4] != namepos
[0][4]:
2069 if equaldirection
and ((not namepos
[0][3] and self
.namevequalize
) or
2070 (not namepos
[0][4] and self
.namehequalize
)):
2071 box
.linealignequal(nameboxes
, labeldist
, -namepos
[0][3], -namepos
[0][4])
2073 for namebox
, np
in zip(nameboxes
, namepos
):
2074 namebox
.linealign(labeldist
, -np
[3], -np
[4])
2075 if self
.basepathattrs
is not None:
2076 p
= axispos
.vbasepath()
2078 ac
.stroke(p
, self
.defaultbasepathattrs
+ self
.basepathattrs
)
2079 if self
.tickattrs
is not None and (self
.innerticklength_str
is not None or
2080 self
.outerticklength_str
is not None):
2081 if self
.innerticklength_str
is not None:
2082 innerticklength
= unit
.length(self
.innerticklength_str
, default_type
="v")
2083 innerticklength_pt
= unit
.topt(innerticklength
)
2084 if unit
.topt(ac
.extent
) < -innerticklength_pt
:
2085 ac
.extent
= -innerticklength
2086 elif self
.outerticklength_str
is not None:
2087 innerticklength
= innerticklength_pt
= 0
2088 if self
.outerticklength_str
is not None:
2089 outerticklength
= unit
.length(self
.outerticklength_str
, default_type
="v")
2090 outerticklength_pt
= unit
.topt(outerticklength
)
2091 if unit
.topt(ac
.extent
) < outerticklength_pt
:
2092 ac
.extent
= outerticklength
2093 elif self
.innerticklength_str
is not None:
2094 outerticklength
= outerticklength_pt
= 0
2095 for pos
in axis
.relsizes
:
2096 if pos
== axis
.relsizes
[0]:
2097 pos
-= axis
.firstdist
2098 elif pos
!= axis
.relsizes
[-1]:
2099 pos
-= 0.5 * axis
.dist
2100 v
= pos
/ axis
.relsizes
[-1]
2101 x
, y
= axispos
.vtickpoint_pt(v
)
2102 dx
, dy
= axispos
.vtickdirection(v
)
2103 x1
= x
+ dx
* innerticklength_pt
2104 y1
= y
+ dy
* innerticklength_pt
2105 x2
= x
- dx
* outerticklength_pt
2106 y2
= y
- dy
* outerticklength_pt
2107 ac
.stroke(path
.line_pt(x1
, y1
, x2
, y2
), self
.defaulttickattrs
+ self
.tickattrs
)
2108 for (v
, x
, y
, dx
, dy
), namebox
in zip(namepos
, nameboxes
):
2109 newextent
= namebox
.extent(dx
, dy
) + labeldist
2110 if unit
.topt(ac
.extent
) < unit
.topt(newextent
):
2111 ac
.extent
= newextent
2112 for namebox
in nameboxes
:
2114 axistitlepainter
.paint(self
, axispos
, axis
, ac
=ac
)
2118 class linkbaraxispainter(baraxispainter
):
2119 """class for painting a linked baraxis
2120 - the inherited baraxispainter is used to paint the axis
2121 - modifies some constructor defaults"""
2123 __implements__
= _Iaxispainter
2125 def __init__(self
, nameattrs
=None, titleattrs
=None, **kwargs
):
2126 """initializes the instance
2127 - the titleattrs default is set to None thus skipping the title
2128 - the nameattrs default is set to None thus skipping the names
2129 - all keyword arguments are passed to axispainter"""
2130 baraxispainter
.__init
__(self
, nameattrs
=nameattrs
, titleattrs
=titleattrs
, **kwargs
)
2133 ################################################################################
2135 ################################################################################
2139 """interface definition of a axis
2140 - an axis should implement an convert and invert method like
2141 _Imap, but this is not part of this interface definition;
2142 one possibility is to mix-in a proper map class, but special
2143 purpose axes might do something else
2144 - an axis has the instance variable axiscanvas after the finish
2146 - an axis might have further instance variables (title, ticks)
2147 to be used in combination with appropriate axispainters"""
2149 def convert(self
, x
):
2150 "convert a value into graph coordinates"
2152 def invert(self
, v
):
2153 "invert a graph coordinate to a axis value"
2155 def getrelsize(self
):
2156 """returns the relative size (width) of the axis
2157 - for use in splitaxis, baraxis etc.
2158 - might return None if no size is available"""
2160 # TODO: describe adjustrange
2161 def setrange(self
, min=None, max=None):
2162 """set the axis data range
2163 - the type of min and max must fit to the axis
2164 - min<max; the axis might be reversed, but this is
2165 expressed internally only (min<max all the time)
2166 - the axis might not apply the change of the range
2167 (e.g. when the axis range is fixed by the user),
2168 but usually the range is extended to contain the
2170 - for invalid parameters (e.g. negativ values at an
2171 logarithmic axis), an exception should be raised
2172 - a RuntimeError is raised, when setrange is called
2173 after the finish method"""
2176 """return data range as a tuple (min, max)
2177 - min<max; the axis might be reversed, but this is
2178 expressed internally only
2179 - a RuntimeError exception is raised when no
2180 range is available"""
2182 def finish(self
, axispos
):
2183 """finishes the axis
2184 - axispos implements _Iaxispos
2185 - sets the instance axiscanvas, which is insertable into the
2186 graph to finally paint the axis
2187 - any modification of the axis range should be disabled after
2188 the finish method was called"""
2189 # TODO: be more specific about exceptions
2191 def createlinkaxis(self
, **kwargs
):
2192 """create a link axis to the axis itself
2193 - typically, a link axis is a axis, which share almost
2194 all properties with the axis it is linked to
2195 - typically, the painter gets replaced by a painter
2196 which doesn't put any text to the axis"""
2200 """base implementation a regular axis
2201 - typical usage is to mix-in a linmap or a logmap to
2202 complete the axis interface
2203 - note that some methods of this class want to access a
2204 parter and a rater; those attributes implementing _Iparter
2205 and _Irater should be initialized by the constructors
2206 of derived classes"""
2208 def __init__(self
, min=None, max=None, reverse
=0, divisor
=1,
2209 title
=None, painter
=axispainter(), texter
=defaulttexter(),
2210 density
=1, maxworse
=2, manualticks
=[]):
2211 """initializes the instance
2212 - min and max fix the axis minimum and maximum, respectively;
2213 they are determined by the data to be plotted, when not fixed
2214 - reverse (boolean) reverses the minimum and the maximum of
2216 - numerical divisor for the axis partitioning
2217 - title is a string containing the axis title
2218 - axispainter is the axis painter (should implement _Ipainter)
2219 - texter is the texter (should implement _Itexter)
2220 - density is a global parameter for the axis paritioning and
2221 axis rating; its default is 1, but the range 0.5 to 2.5 should
2222 be usefull to get less or more ticks by the automatic axis
2224 - maxworse is a number of trials with worse tick rating
2225 before giving up (usually it should not be needed to increase
2226 this value; increasing the number will slow down the automatic
2227 axis partitioning considerably)
2228 - manualticks and the partitioner results are mixed
2230 - note that some methods of this class want to access a
2231 parter and a rater; those attributes implementing _Iparter
2232 and _Irater should be initialized by the constructors
2233 of derived classes"""
2234 if min is not None and max is not None and min > max:
2235 min, max, reverse
= max, min, not reverse
2236 self
.fixmin
, self
.fixmax
, self
.min, self
.max, self
.reverse
= min is not None, max is not None, min, max, reverse
2237 self
.divisor
= divisor
2239 self
.painter
= painter
2240 self
.texter
= texter
2241 self
.density
= density
2242 self
.maxworse
= maxworse
2243 self
.manualticks
= self
.checkfraclist(manualticks
)
2245 self
.axiscanvas
= None
2248 def _setrange(self
, min=None, max=None):
2249 if not self
.fixmin
and min is not None and (self
.min is None or min < self
.min):
2251 if not self
.fixmax
and max is not None and (self
.max is None or max > self
.max):
2253 if None not in (self
.min, self
.max):
2256 self
.setbasepoints(((self
.min, 1), (self
.max, 0)))
2258 self
.setbasepoints(((self
.min, 0), (self
.max, 1)))
2260 def _getrange(self
):
2261 return self
.min, self
.max
2263 def _forcerange(self
, range):
2264 self
.min, self
.max = range
2267 def setrange(self
, min=None, max=None):
2268 oldmin
, oldmax
= self
.min, self
.max
2269 self
._setrange
(min, max)
2270 if self
.axiscanvas
is not None and ((oldmin
!= self
.min) or (oldmax
!= self
.max)):
2271 raise RuntimeError("range modification while axis was already finished")
2275 def adjustrange(self
, points
, index
, deltaindex
=None, deltaminindex
=None, deltamaxindex
=None):
2277 if len([x
for x
in [deltaindex
, deltaminindex
, deltamaxindex
] if x
is not None]) > 1:
2278 raise RuntimeError("only one of delta???index should set")
2279 if deltaindex
is not None:
2280 deltaminindex
= deltamaxindex
= deltaindex
2281 if deltaminindex
is not None:
2282 for point
in points
:
2284 value
= point
[index
] - point
[deltaminindex
] + self
.zero
2288 if min is None or value
< min: min = value
2289 if max is None or value
> max: max = value
2290 elif deltamaxindex
is not None:
2291 for point
in points
:
2293 value
= point
[index
] + point
[deltamaxindex
] + self
.zero
2297 if min is None or value
< min: min = value
2298 if max is None or value
> max: max = value
2300 for point
in points
:
2302 value
= point
[index
] + self
.zero
2306 if min is None or value
< min: min = value
2307 if max is None or value
> max: max = value
2308 self
.setrange(min, max)
2311 if self
.min is not None and self
.max is not None:
2312 return self
.min, self
.max
2314 def checkfraclist(self
, fracs
):
2315 "orders a list of fracs, equal entries are not allowed"
2316 if not len(fracs
): return []
2317 sorted = list(fracs
)
2320 for item
in sorted[1:]:
2322 raise ValueError("duplicate entry found")
2326 def finish(self
, axispos
):
2327 if self
.axiscanvas
is not None: return
2329 # lesspart and morepart can be called after defaultpart;
2330 # this works although some axes may share their autoparting,
2331 # because the axes are processed sequentially
2333 if self
.parter
is not None:
2334 min, max = self
.getrange()
2335 self
.ticks
= _mergeticklists(self
.manualticks
,
2336 self
.parter
.defaultpart(min/self
.divisor
,
2341 nextpart
= self
.parter
.lesspart
2342 while nextpart
is not None:
2343 newticks
= nextpart()
2344 if newticks
is not None:
2345 newticks
= _mergeticklists(self
.manualticks
, newticks
)
2347 bestrate
= self
.rater
.rateticks(self
, self
.ticks
, self
.density
)
2348 bestrate
+= self
.rater
.raterange(self
.convert(float(self
.ticks
[-1])/self
.divisor
)-
2349 self
.convert(float(self
.ticks
[0])/self
.divisor
), 1)
2350 variants
= [[bestrate
, self
.ticks
]]
2352 newrate
= self
.rater
.rateticks(self
, newticks
, self
.density
)
2353 newrate
+= self
.rater
.raterange(self
.convert(float(newticks
[-1])/self
.divisor
)-
2354 self
.convert(float(newticks
[0])/self
.divisor
), 1)
2355 variants
.append([newrate
, newticks
])
2356 if newrate
< bestrate
:
2363 if worse
== self
.maxworse
and nextpart
== self
.parter
.lesspart
:
2365 nextpart
= self
.parter
.morepart
2366 if worse
== self
.maxworse
and nextpart
== self
.parter
.morepart
:
2369 self
.ticks
=self
.manualticks
2371 # rating, when several choises are available
2374 if self
.painter
is not None:
2377 while i
< len(variants
) and (bestrate
is None or variants
[i
][0] < bestrate
):
2378 saverange
= self
._getrange
()
2379 self
.ticks
= variants
[i
][1]
2381 self
.setrange(float(self
.ticks
[0])*self
.divisor
, float(self
.ticks
[-1])*self
.divisor
)
2382 self
.texter
.labels(self
.ticks
)
2383 ac
= self
.painter
.paint(axispos
, self
)
2384 ratelayout
= self
.rater
.ratelayout(ac
, self
.density
)
2385 if ratelayout
is not None:
2386 variants
[i
][0] += ratelayout
2387 variants
[i
].append(ac
)
2389 variants
[i
][0] = None
2390 if variants
[i
][0] is not None and (bestrate
is None or variants
[i
][0] < bestrate
):
2391 bestrate
= variants
[i
][0]
2392 self
._forcerange
(saverange
)
2394 if bestrate
is None:
2395 raise RuntimeError("no valid axis partitioning found")
2396 variants
= [variant
for variant
in variants
[:i
] if variant
[0] is not None]
2398 self
.ticks
= variants
[0][1]
2400 self
.setrange(float(self
.ticks
[0])*self
.divisor
, float(self
.ticks
[-1])*self
.divisor
)
2401 self
.axiscanvas
= variants
[0][2]
2403 self
.ticks
= variants
[0][1]
2404 self
.texter
.labels(self
.ticks
)
2406 self
.setrange(float(self
.ticks
[0])*self
.divisor
, float(self
.ticks
[-1])*self
.divisor
)
2407 self
.axiscanvas
= axiscanvas()
2410 self
.setrange(float(self
.ticks
[0])*self
.divisor
, float(self
.ticks
[-1])*self
.divisor
)
2411 self
.texter
.labels(self
.ticks
)
2412 if self
.painter
is not None:
2413 self
.axiscanvas
= self
.painter
.paint(axispos
, self
)
2415 self
.axiscanvas
= axiscanvas()
2417 def createlinkaxis(self
, **args
):
2418 return linkaxis(self
, **args
)
2421 class linaxis(_axis
, _linmap
):
2422 """implementation of a linear axis"""
2424 __implements__
= _Iaxis
2426 def __init__(self
, parter
=autolinparter(), rater
=axisrater(), **args
):
2427 """initializes the instance
2428 - the parter attribute implements _Iparter
2429 - manualticks and the partitioner results are mixed
2431 - the rater implements _Irater and is used to rate different
2432 tick lists created by the partitioner (after merging with
2434 - futher keyword arguments are passed to _axis"""
2435 _axis
.__init
__(self
, **args
)
2436 if self
.fixmin
and self
.fixmax
:
2437 self
.relsize
= self
.max - self
.min
2438 self
.parter
= parter
2442 class logaxis(_axis
, _logmap
):
2443 """implementation of a logarithmic axis"""
2445 __implements__
= _Iaxis
2447 def __init__(self
, parter
=autologparter(), rater
=axisrater(ticks
=axisrater
.logticks
, labels
=axisrater
.loglabels
), **args
):
2448 """initializes the instance
2449 - the parter attribute implements _Iparter
2450 - manualticks and the partitioner results are mixed
2452 - the rater implements _Irater and is used to rate different
2453 tick lists created by the partitioner (after merging with
2455 - futher keyword arguments are passed to _axis"""
2456 _axis
.__init
__(self
, **args
)
2457 if self
.fixmin
and self
.fixmax
:
2458 self
.relsize
= math
.log(self
.max) - math
.log(self
.min)
2459 self
.parter
= parter
2464 """a axis linked to an already existing regular axis
2465 - almost all properties of the axis are "copied" from the
2466 axis this axis is linked to
2467 - usually, linked axis are used to create an axis to an
2468 existing axis with different painting properties; linked
2469 axis can be used to plot an axis twice at the opposite
2470 sides of a graphxy or even to share an axis between
2471 different graphs!"""
2473 __implements__
= _Iaxis
2475 def __init__(self
, linkedaxis
, painter
=linkaxispainter()):
2476 """initializes the instance
2477 - it gets a axis this linkaxis is linked to
2478 - it gets a painter to be used for this linked axis"""
2479 self
.linkedaxis
= linkedaxis
2480 self
.painter
= painter
2481 self
.axiscanvas
= None
2483 def __getattr__(self
, attr
):
2484 """access to unkown attributes are handed over to the
2485 axis this linkaxis is linked to"""
2486 return getattr(self
.linkedaxis
, attr
)
2488 def finish(self
, axispos
):
2489 """finishes the axis
2490 - instead of performing the hole finish process
2491 (paritioning, rating, etc.) just a painter call
2493 if self
.axiscanvas
is None:
2494 if self
.linkedaxis
.axiscanvas
is None:
2495 raise RuntimeError("link axis finish method called before the finish method of the original axis")
2496 self
.axiscanvas
= self
.painter
.paint(axispos
, self
)
2500 """implementation of a split axis
2501 - a split axis contains several (sub-)axes with
2502 non-overlapping data ranges -- between these subaxes
2503 the axis is "splitted"
2504 - (just to get sure: a splitaxis can contain other
2505 splitaxes as its subaxes)
2506 - a splitaxis implements the _Iaxispos for its subaxes
2507 by inheritance from _subaxispos"""
2509 __implements__
= _Iaxis
, _Iaxispos
2511 def __init__(self
, subaxes
, splitlist
=[0.5], splitdist
=0.1, relsizesplitdist
=1,
2512 title
=None, painter
=splitaxispainter()):
2513 """initializes the instance
2514 - subaxes is a list of subaxes
2515 - splitlist is a list of graph coordinates, where the splitting
2516 of the main axis should be performed; if the list isn't long enough
2517 for the subaxes, missing entries are considered to be None
2518 - splitdist is the size of the splitting in graph coordinates, when
2519 the associated splitlist entry is not None
2520 - relsizesplitdist: a None entry in splitlist means, that the
2521 position of the splitting should be calculated out of the
2522 relsize values of conrtibuting subaxes (the size of the
2523 splitting is relsizesplitdist in values of the relsize values
2525 - title is the title of the axis as a string
2526 - painter is the painter of the axis; it should be specialized to
2528 - the relsize of the splitaxis is the sum of the relsizes of the
2529 subaxes including the relsizesplitdist"""
2530 self
.subaxes
= subaxes
2531 self
.painter
= painter
2533 self
.splitlist
= splitlist
2534 for subaxis
in self
.subaxes
:
2537 self
.subaxes
[0].vmin
= 0
2538 self
.subaxes
[0].vminover
= None
2539 self
.subaxes
[-1].vmax
= 1
2540 self
.subaxes
[-1].vmaxover
= None
2541 for i
in xrange(len(self
.splitlist
)):
2542 if self
.splitlist
[i
] is not None:
2543 self
.subaxes
[i
].vmax
= self
.splitlist
[i
] - 0.5*splitdist
2544 self
.subaxes
[i
].vmaxover
= self
.splitlist
[i
]
2545 self
.subaxes
[i
+1].vmin
= self
.splitlist
[i
] + 0.5*splitdist
2546 self
.subaxes
[i
+1].vminover
= self
.splitlist
[i
]
2548 while i
< len(self
.subaxes
):
2549 if self
.subaxes
[i
].vmax
is None:
2550 j
= relsize
= relsize2
= 0
2551 while self
.subaxes
[i
+ j
].vmax
is None:
2552 relsize
+= self
.subaxes
[i
+ j
].relsize
+ relsizesplitdist
2554 relsize
+= self
.subaxes
[i
+ j
].relsize
2555 vleft
= self
.subaxes
[i
].vmin
2556 vright
= self
.subaxes
[i
+ j
].vmax
2557 for k
in range(i
, i
+ j
):
2558 relsize2
+= self
.subaxes
[k
].relsize
2559 self
.subaxes
[k
].vmax
= vleft
+ (vright
- vleft
) * relsize2
/ float(relsize
)
2560 relsize2
+= 0.5 * relsizesplitdist
2561 self
.subaxes
[k
].vmaxover
= self
.subaxes
[k
+ 1].vminover
= vleft
+ (vright
- vleft
) * relsize2
/ float(relsize
)
2562 relsize2
+= 0.5 * relsizesplitdist
2563 self
.subaxes
[k
+1].vmin
= vleft
+ (vright
- vleft
) * relsize2
/ float(relsize
)
2564 if i
== 0 and i
+ j
+ 1 == len(self
.subaxes
):
2565 self
.relsize
= relsize
2570 self
.fixmin
= self
.subaxes
[0].fixmin
2572 self
.min = self
.subaxes
[0].min
2573 self
.fixmax
= self
.subaxes
[-1].fixmax
2575 self
.max = self
.subaxes
[-1].max
2577 self
.axiscanvas
= None
2580 min = self
.subaxes
[0].getrange()
2581 max = self
.subaxes
[-1].getrange()
2583 return min[0], max[1]
2587 def setrange(self
, min, max):
2588 self
.subaxes
[0].setrange(min, None)
2589 self
.subaxes
[-1].setrange(None, max)
2591 def convert(self
, value
):
2592 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2593 if value
< self
.subaxes
[0].max:
2594 return self
.subaxes
[0].vmin
+ self
.subaxes
[0].convert(value
)*(self
.subaxes
[0].vmax
-self
.subaxes
[0].vmin
)
2595 for axis
in self
.subaxes
[1:-1]:
2596 if value
> axis
.min and value
< axis
.max:
2597 return axis
.vmin
+ axis
.convert(value
)*(axis
.vmax
-axis
.vmin
)
2598 if value
> self
.subaxes
[-1].min:
2599 return self
.subaxes
[-1].vmin
+ self
.subaxes
[-1].convert(value
)*(self
.subaxes
[-1].vmax
-self
.subaxes
[-1].vmin
)
2600 raise ValueError("value couldn't be assigned to a split region")
2602 def finish(self
, axispos
):
2603 if self
.axiscanvas
is None:
2604 self
.axiscanvas
= self
.painter
.paint(axispos
, self
)
2606 def createlinkaxis(self
, **args
):
2607 return linksplitaxis(self
, **args
)
2610 class omitsubaxispainter
: pass
2612 class linksplitaxis(linkaxis
):
2613 """a splitaxis linked to an already existing splitaxis
2614 - inherits the access to a linked axis -- as before,
2615 basically only the painter is replaced
2616 - it takes care of the creation of linked axes of
2619 __implements__
= _Iaxis
2621 def __init__(self
, linkedaxis
, painter
=linksplitaxispainter(), subaxispainter
=omitsubaxispainter
):
2622 """initializes the instance
2623 - linkedaxis is the axis this axis becomes linked to
2624 - painter is axispainter instance for this linked axis
2625 - subaxispainter is a changeable painter to be used for linked
2626 subaxes; if omitsubaxispainter the createlinkaxis method of
2627 the subaxis are called without a painter parameter"""
2628 linkaxis
.__init
__(self
, linkedaxis
, painter
=painter
)
2630 for subaxis
in linkedaxis
.subaxes
:
2631 painter
= attr
.selectattr(subaxispainter
, len(self
.subaxes
), len(linkedaxis
.subaxes
))
2632 if painter
is omitsubaxispainter
:
2633 self
.subaxes
.append(subaxis
.createlinkaxis())
2635 self
.subaxes
.append(subaxis
.createlinkaxis(painter
=painter
))
2639 """implementation of a axis for bar graphs
2640 - a bar axes is different from a splitaxis by the way it
2641 selects its subaxes: the convert method gets a list,
2642 where the first entry is a name selecting a subaxis out
2643 of a list; instead of the term "bar" or "subaxis" the term
2644 "item" will be used here
2645 - the baraxis stores a list of names be identify the items;
2646 the names might be of any time (strings, integers, etc.);
2647 the names can be printed as the titles for the items, but
2648 alternatively the names might be transformed by the texts
2649 dictionary, which maps a name to a text to be used to label
2650 the items in the painter
2651 - usually, there is only one subaxis, which is used as
2652 the subaxis for all items
2653 - alternatively it is also possible to use another baraxis
2654 as a multisubaxis; it is copied via the createsubaxis
2655 method whenever another subaxis is needed (by that a
2656 nested bar axis with a different number of subbars at
2657 each item can be created)
2658 - any axis can be a subaxis of a baraxis; if no subaxis
2659 is specified at all, the baraxis simulates a linear
2660 subaxis with a fixed range of 0 to 1
2661 - a splitaxis implements the _Iaxispos for its subaxes
2662 by inheritance from _subaxispos when the multisubaxis
2663 feature is turned on"""
2665 def __init__(self
, subaxis
=None, multisubaxis
=None, title
=None,
2666 dist
=0.5, firstdist
=None, lastdist
=None, names
=None,
2667 texts
={}, painter
=baraxispainter()):
2668 """initialize the instance
2669 - subaxis contains a axis to be used as the subaxis
2671 - multisubaxis might contain another baraxis instance
2672 to be used to construct a new subaxis for each item;
2673 (by that a nested bar axis with a different number
2674 of subbars at each item can be created)
2675 - only one of subaxis or multisubaxis can be set; if neither
2676 of them is set, the baraxis behaves like having a linaxis
2677 as its subaxis with a fixed range 0 to 1
2678 - the title attribute contains the axis title as a string
2679 - the dist is a relsize to be used as the distance between
2681 - the firstdist and lastdist are the distance before the
2682 first and after the last item, respectively; when set
2683 to None (the default), 0.5*dist is used
2684 - names is a predefined list of names to identify the
2685 items; if set, the name list is fixed
2686 - texts is a dictionary transforming a name to a text in
2687 the painter; if a name isn't found in the dictionary
2689 - the relsize of the baraxis is the sum of the
2690 relsizes including all distances between the items"""
2692 if firstdist
is not None:
2693 self
.firstdist
= firstdist
2695 self
.firstdist
= 0.5 * dist
2696 if lastdist
is not None:
2697 self
.lastdist
= lastdist
2699 self
.lastdist
= 0.5 * dist
2700 self
.relsizes
= None
2703 for name
in helper
.ensuresequence(names
):
2705 self
.fixnames
= names
is not None
2706 self
.multisubaxis
= multisubaxis
2707 if self
.multisubaxis
is not None:
2708 if subaxis
is not None:
2709 raise RuntimeError("either use subaxis or multisubaxis")
2710 self
.subaxis
= [self
.createsubaxis() for name
in self
.names
]
2712 self
.subaxis
= subaxis
2716 self
.painter
= painter
2717 self
.axiscanvas
= None
2719 def createsubaxis(self
):
2720 return baraxis(subaxis
=self
.multisubaxis
.subaxis
,
2721 multisubaxis
=self
.multisubaxis
.multisubaxis
,
2722 title
=self
.multisubaxis
.title
,
2723 dist
=self
.multisubaxis
.dist
,
2724 firstdist
=self
.multisubaxis
.firstdist
,
2725 lastdist
=self
.multisubaxis
.lastdist
,
2726 names
=self
.multisubaxis
.names
,
2727 texts
=self
.multisubaxis
.texts
,
2728 painter
=self
.multisubaxis
.painter
)
2731 # TODO: we do not yet have a proper range handling for a baraxis
2734 def setrange(self
, min=None, max=None):
2735 # TODO: we do not yet have a proper range handling for a baraxis
2736 raise RuntimeError("range handling for a baraxis is not implemented")
2738 def setname(self
, name
, *subnames
):
2739 """add a name to identify an item at the baraxis
2740 - by using subnames, nested name definitions are
2742 - a style (or the user itself) might use this to
2743 insert new items into a baraxis
2744 - setting self.relsizes to None forces later recalculation"""
2745 if not self
.fixnames
:
2746 if name
not in self
.names
:
2747 self
.relsizes
= None
2748 self
.names
.append(name
)
2749 if self
.multisubaxis
is not None:
2750 self
.subaxis
.append(self
.createsubaxis())
2751 if (not self
.fixnames
or name
in self
.names
) and len(subnames
):
2752 if self
.multisubaxis
is not None:
2753 if self
.subaxis
[self
.names
.index(name
)].setname(*subnames
):
2754 self
.relsizes
= None
2756 if self
.subaxis
.setname(*subnames
):
2757 self
.relsizes
= None
2758 return self
.relsizes
is not None
2760 def updaterelsizes(self
):
2761 # guess what it does: it recalculates relsize attribute
2762 self
.relsizes
= [i
*self
.dist
+ self
.firstdist
for i
in range(len(self
.names
) + 1)]
2763 self
.relsizes
[-1] += self
.lastdist
- self
.dist
2764 if self
.multisubaxis
is not None:
2766 for i
in range(1, len(self
.relsizes
)):
2767 self
.subaxis
[i
-1].updaterelsizes()
2768 subrelsize
+= self
.subaxis
[i
-1].relsizes
[-1]
2769 self
.relsizes
[i
] += subrelsize
2771 if self
.subaxis
is None:
2774 self
.subaxis
.updaterelsizes()
2775 subrelsize
= self
.subaxis
.relsizes
[-1]
2776 for i
in range(1, len(self
.relsizes
)):
2777 self
.relsizes
[i
] += i
* subrelsize
2779 def convert(self
, value
):
2780 """baraxis convert method
2781 - the value should be a list, where the first entry is
2782 a member of the names (set in the constructor or by the
2783 setname method); this first entry identifies an item in
2785 - following values are passed to the appropriate subaxis
2787 - when there is no subaxis, the convert method will behave
2788 like having a linaxis from 0 to 1 as subaxis"""
2789 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2790 if not self
.relsizes
:
2791 self
.updaterelsizes()
2792 pos
= self
.names
.index(value
[0])
2794 if self
.subaxis
is None:
2797 if self
.multisubaxis
is not None:
2798 subvalue
= value
[1] * self
.subaxis
[pos
].relsizes
[-1]
2800 subvalue
= value
[1] * self
.subaxis
.relsizes
[-1]
2802 if self
.multisubaxis
is not None:
2803 subvalue
= self
.subaxis
[pos
].convert(value
[1:]) * self
.subaxis
[pos
].relsizes
[-1]
2805 subvalue
= self
.subaxis
.convert(value
[1:]) * self
.subaxis
.relsizes
[-1]
2806 return (self
.relsizes
[pos
] + subvalue
) / float(self
.relsizes
[-1])
2808 def finish(self
, axispos
):
2809 if self
.axiscanvas
is None:
2810 if self
.multisubaxis
is not None:
2811 for name
, subaxis
in zip(self
.names
, self
.subaxis
):
2812 subaxis
.vmin
= self
.convert((name
, 0))
2813 subaxis
.vmax
= self
.convert((name
, 1))
2814 self
.axiscanvas
= self
.painter
.paint(axispos
, self
)
2816 def createlinkaxis(self
, **args
):
2817 return linkbaraxis(self
, **args
)
2820 class linkbaraxis(linkaxis
):
2821 """a baraxis linked to an already existing baraxis
2822 - inherits the access to a linked axis -- as before,
2823 basically only the painter is replaced
2824 - it must take care of the creation of linked axes of
2827 __implements__
= _Iaxis
2829 def __init__(self
, linkedaxis
, painter
=linkbaraxispainter()):
2830 """initializes the instance
2831 - it gets a axis this linkaxis is linked to
2832 - it gets a painter to be used for this linked axis"""
2833 linkaxis
.__init
__(self
, linkedaxis
, painter
=painter
)
2834 if self
.multisubaxis
is not None:
2835 self
.subaxis
= [subaxis
.createlinkaxis() for subaxis
in self
.linkedaxis
.subaxis
]
2836 elif self
.subaxis
is not None:
2837 self
.subaxis
= self
.subaxis
.createlinkaxis()
2840 def pathaxis(path
, axis
, **kwargs
):
2841 """creates an axiscanvas for an axis along a path"""
2842 mypathaxispos
= pathaxispos(path
, axis
.convert
, **kwargs
)
2843 axis
.finish(mypathaxispos
)
2844 return axis
.axiscanvas
2846 ################################################################################
2848 ################################################################################
2851 # g = graph.graphxy(key=graph.key())
2852 # g.addkey(graph.key(), ...)
2857 defaulttextattrs
= [textmodule
.vshift
.mathaxis
]
2859 def __init__(self
, dist
="0.2 cm", pos
="tr", hinside
=1, vinside
=1, hdist
="0.6 cm", vdist
="0.4 cm",
2860 symbolwidth
="0.5 cm", symbolheight
="0.25 cm", symbolspace
="0.2 cm",
2862 self
.dist_str
= dist
2864 self
.hinside
= hinside
2865 self
.vinside
= vinside
2866 self
.hdist_str
= hdist
2867 self
.vdist_str
= vdist
2868 self
.symbolwidth_str
= symbolwidth
2869 self
.symbolheight_str
= symbolheight
2870 self
.symbolspace_str
= symbolspace
2871 self
.textattrs
= textattrs
2872 self
.plotinfos
= None
2873 if self
.pos
in ("tr", "rt"):
2876 elif self
.pos
in ("br", "rb"):
2879 elif self
.pos
in ("tl", "lt"):
2882 elif self
.pos
in ("bl", "lb"):
2886 raise RuntimeError("invalid pos attribute")
2888 def setplotinfos(self
, *plotinfos
):
2889 """set the plotinfos to be used in the key
2890 - call it exactly once
2891 - plotinfo instances with title == None are ignored"""
2892 if self
.plotinfos
is not None:
2893 raise RuntimeError("setplotinfo is called multiple times")
2894 self
.plotinfos
= [plotinfo
for plotinfo
in plotinfos
if plotinfo
.data
.title
is not None]
2896 def dolayout(self
, graph
):
2897 "creates the layout of the key"
2898 self
.dist_pt
= unit
.topt(unit
.length(self
.dist_str
, default_type
="v"))
2899 self
.hdist_pt
= unit
.topt(unit
.length(self
.hdist_str
, default_type
="v"))
2900 self
.vdist_pt
= unit
.topt(unit
.length(self
.vdist_str
, default_type
="v"))
2901 self
.symbolwidth_pt
= unit
.topt(unit
.length(self
.symbolwidth_str
, default_type
="v"))
2902 self
.symbolheight_pt
= unit
.topt(unit
.length(self
.symbolheight_str
, default_type
="v"))
2903 self
.symbolspace_pt
= unit
.topt(unit
.length(self
.symbolspace_str
, default_type
="v"))
2905 for plotinfo
in self
.plotinfos
:
2906 self
.titles
.append(graph
.texrunner
.text_pt(0, 0, plotinfo
.data
.title
, self
.defaulttextattrs
+ self
.textattrs
))
2907 box
.tile_pt(self
.titles
, self
.dist_pt
, 0, -1)
2908 box
.linealignequal_pt(self
.titles
, self
.symbolwidth_pt
+ self
.symbolspace_pt
, 1, 0)
2911 """return a bbox for the key
2912 method should be called after dolayout"""
2914 for title
in self
.titles
:
2915 titlebbox
= title
.bbox() + bbox
._bbox
(0, title
.center
[1] - 0.5 * self
.symbolheight_pt
,
2916 0, title
.center
[1] + 0.5 * self
.symbolheight_pt
)
2923 def paint(self
, c
, x
, y
):
2924 """paint the graph key into a canvas c at the position x and y (in postscript points)
2925 - method should be called after dolayout
2926 - the x, y alignment might be calculated by the graph using:
2927 - the bbox of the key as returned by the keys bbox method
2928 - the attributes hdist_pt, vdist_pt, hinside, and vinside of the key
2929 - the dimension and geometry of the graph"""
2930 sc
= c
.insert(canvas
.canvas(trafomodule
.translate_pt(x
, y
)))
2931 for plotinfo
, title
in zip(self
.plotinfos
, self
.titles
):
2932 plotinfo
.style
.key(sc
, 0, -0.5 * self
.symbolheight_pt
+ title
.center
[1],
2933 self
.symbolwidth_pt
, self
.symbolheight_pt
)
2937 ################################################################################
2939 ################################################################################
2943 """an axispos linear along a line with a fix direction for the ticks"""
2945 __implements__
= _Iaxispos
2947 def __init__(self
, convert
, x1
, y1
, x2
, y2
, fixtickdirection
):
2948 """initializes the instance
2949 - only the convert method is needed from the axis
2950 - x1, y1, x2, y2 are PyX lengths (start and end position of the line)
2951 - fixtickdirection is a tuple tick direction (fixed along the line)"""
2952 self
.convert
= convert
2957 self
.x1_pt
= unit
.topt(x1
)
2958 self
.y1_pt
= unit
.topt(y1
)
2959 self
.x2_pt
= unit
.topt(x2
)
2960 self
.y2_pt
= unit
.topt(y2
)
2961 self
.fixtickdirection
= fixtickdirection
2963 def vbasepath(self
, v1
=None, v2
=None):
2968 return path
.line_pt((1-v1
)*self
.x1_pt
+v1
*self
.x2_pt
,
2969 (1-v1
)*self
.y1_pt
+v1
*self
.y2_pt
,
2970 (1-v2
)*self
.x1_pt
+v2
*self
.x2_pt
,
2971 (1-v2
)*self
.y1_pt
+v2
*self
.y2_pt
)
2973 def basepath(self
, x1
=None, x2
=None):
2977 v1
= self
.convert(x1
)
2981 v2
= self
.convert(x2
)
2982 return path
.line_pt((1-v1
)*self
.x1_pt
+v1
*self
.x2_pt
,
2983 (1-v1
)*self
.y1_pt
+v1
*self
.y2_pt
,
2984 (1-v2
)*self
.x1_pt
+v2
*self
.x2_pt
,
2985 (1-v2
)*self
.y1_pt
+v2
*self
.y2_pt
)
2987 def gridpath(self
, x
):
2988 raise RuntimeError("gridpath not available")
2990 def vgridpath(self
, v
):
2991 raise RuntimeError("gridpath not available")
2993 def vtickpoint_pt(self
, v
):
2994 return (1-v
)*self
.x1_pt
+v
*self
.x2_pt
, (1-v
)*self
.y1_pt
+v
*self
.y2_pt
2996 def vtickpoint(self
, v
):
2997 return (1-v
)*self
.x1
+v
*self
.x2
, (1-v
)*self
.y1
+v
*self
.y2
2999 def tickpoint_pt(self
, x
):
3001 return (1-v
)*self
.x1_pt
+v
*self
.x2_pt
, (1-v
)*self
.y1_pt
+v
*self
.y2_pt
3003 def tickpoint(self
, x
):
3005 return (1-v
)*self
.x1
+v
*self
.x2
, (1-v
)*self
.y1
+v
*self
.y2
3007 def tickdirection(self
, x
):
3008 return self
.fixtickdirection
3010 def vtickdirection(self
, v
):
3011 return self
.fixtickdirection
3014 class lineaxisposlinegrid(lineaxispos
):
3015 """an axispos linear along a line with a fix direction for the ticks
3016 with support for grid lines for a rectangular graphs"""
3018 __implements__
= _Iaxispos
3020 def __init__(self
, convert
, x1
, y1
, x2
, y2
, fixtickdirection
, startgridlength
, endgridlength
):
3021 """initializes the instance
3022 - only the convert method is needed from the axis
3023 - x1, y1, x2, y2 are PyX lengths (start and end position of the line)
3024 - fixtickdirection is a tuple tick direction (fixed along the line)
3025 - startgridlength and endgridlength are PyX lengths for the starting
3026 and end point of the grid, respectively; the gridpath is a line along
3027 the fixtickdirection"""
3028 lineaxispos
.__init
__(self
, convert
, x1
, y1
, x2
, y2
, fixtickdirection
)
3029 self
.startgridlength
= startgridlength
3030 self
.endgridlength
= endgridlength
3031 self
.startgridlength_pt
= unit
.topt(self
.startgridlength
)
3032 self
.endgridlength_pt
= unit
.topt(self
.endgridlength
)
3034 def gridpath(self
, x
):
3036 return path
.line_pt((1-v
)*self
.x1_pt
+v
*self
.x2_pt
+self
.fixtickdirection
[0]*self
.startgridlength_pt
,
3037 (1-v
)*self
.y1_pt
+v
*self
.y2_pt
+self
.fixtickdirection
[1]*self
.startgridlength_pt
,
3038 (1-v
)*self
.x1_pt
+v
*self
.x2_pt
+self
.fixtickdirection
[0]*self
.endgridlength_pt
,
3039 (1-v
)*self
.y1_pt
+v
*self
.y2_pt
+self
.fixtickdirection
[1]*self
.endgridlength_pt
)
3041 def vgridpath(self
, v
):
3042 return path
.line_pt((1-v
)*self
.x1_pt
+v
*self
.x2_pt
+self
.fixtickdirection
[0]*self
.startgridlength_pt
,
3043 (1-v
)*self
.y1_pt
+v
*self
.y2_pt
+self
.fixtickdirection
[1]*self
.startgridlength_pt
,
3044 (1-v
)*self
.x1_pt
+v
*self
.x2_pt
+self
.fixtickdirection
[0]*self
.endgridlength_pt
,
3045 (1-v
)*self
.y1_pt
+v
*self
.y2_pt
+self
.fixtickdirection
[1]*self
.endgridlength_pt
)
3048 class graphxy(canvas
.canvas
):
3050 axisnames
= "x", "y"
3054 def __init__(self
, type, axispos
, tickdirection
):
3056 - type == 0: x-axis; type == 1: y-axis
3057 - axispos_pt is the y or x position of the x-axis or y-axis
3058 in postscript points, respectively
3059 - axispos is analogous to axispos, but as a PyX length
3060 - dx and dy is the tick direction
3063 self
.axispos
= axispos
3064 self
.axispos_pt
= unit
.topt(axispos
)
3065 self
.tickdirection
= tickdirection
3067 def clipcanvas(self
):
3068 return self
.insert(canvas
.canvas(canvas
.clip(path
.rect_pt(self
.xpos_pt
, self
.ypos_pt
, self
.width_pt
, self
.height_pt
))))
3070 def plot(self
, data
, style
=None):
3072 raise RuntimeError("layout setup was already performed")
3083 style
= d
.defaultstyle
3084 elif style
!= d
.defaultstyle
:
3085 raise RuntimeError("defaultstyles differ")
3088 self
.plotdata
.append(d
)
3091 def addkey(self
, key
, *plotinfos
):
3093 raise RuntimeError("layout setup was already performed")
3094 self
.addkeys
.append((key
, plotinfos
))
3096 def pos_pt(self
, x
, y
, xaxis
=None, yaxis
=None):
3098 xaxis
= self
.axes
["x"]
3100 yaxis
= self
.axes
["y"]
3101 return self
.xpos_pt
+xaxis
.convert(x
)*self
.width_pt
, self
.ypos_pt
+yaxis
.convert(y
)*self
.height_pt
3103 def pos(self
, x
, y
, xaxis
=None, yaxis
=None):
3105 xaxis
= self
.axes
["x"]
3107 yaxis
= self
.axes
["y"]
3108 return self
.xpos
+xaxis
.convert(x
)*self
.width
, self
.ypos
+yaxis
.convert(y
)*self
.height
3110 def vpos_pt(self
, vx
, vy
):
3111 return self
.xpos_pt
+vx
*self
.width_pt
, self
.ypos_pt
+vy
*self
.height_pt
3113 def vpos(self
, vx
, vy
):
3114 return self
.xpos
+vx
*self
.width
, self
.ypos
+vy
*self
.height
3116 def geodesic(self
, x1
, y1
, x2
, y2
, xaxis
=None, yaxis
=None):
3118 xaxis
= self
.axes
["x"]
3120 yaxis
= self
.axes
["y"]
3121 return path
.line_pt(self
.xpos_pt
+xaxis
.convert(x1
)*self
.width_pt
,
3122 self
.ypos_pt
+yaxis
.convert(y1
)*self
.height_pt
,
3123 self
.xpos_pt
+xaxis
.convert(x2
)*self
.width_pt
,
3124 self
.ypos_pt
+yaxis
.convert(y2
)*self
.height_pt
)
3126 def vgeodesic(self
, vx1
, vy1
, vx2
, vy2
):
3127 return path
.line_pt(self
.xpos_pt
+vx1
*self
.width_pt
,
3128 self
.ypos_pt
+vy1
*self
.height_pt
,
3129 self
.xpos_pt
+vx2
*self
.width_pt
,
3130 self
.ypos_pt
+vy2
*self
.height_pt
)
3132 def isometric_pt(self
, x
, y
, direction
, start_pt
, end_pt
, xaxis
=None, yaxis
=None):
3134 xaxis
= self
.axes
["x"]
3136 yaxis
= self
.axes
["y"]
3137 xpos
= self
.xpos_pt
+xaxis
.convert(x
)*self
.width_pt
3138 ypos
= self
.ypos_pt
+yaxis
.convert(y
)*self
.height_pt
3139 if direction
== "x":
3140 return path
.line_pt(xpos
+ start_pt
, ypos
, xpos
+ end_pt
, ypos
)
3141 elif direction
== "y":
3142 return path
.line_pt(xpos
, ypos
+ start_pt
, xpos
, ypos
+ end_pt
)
3143 raise RuntimeError("invalid isometric direction '%s'" % direction
)
3145 def isometric(self
, x
, y
, direction
, start
, end
, xaxis
=None, yaxis
=None):
3147 xaxis
= self
.axes
["x"]
3149 yaxis
= self
.axes
["y"]
3150 xpos
= self
.xpos_pt
+xaxis
.convert(x
)*self
.width_pt
3151 ypos
= self
.ypos_pt
+yaxis
.convert(y
)*self
.height_pt
3152 if direction
== "x":
3153 return path
.line_pt(xpos
+ unit
.topt(start
), ypos
, xpos
+ unit
.topt(end
), ypos
)
3154 elif direction
== "y":
3155 return path
.line_pt(xpos
, ypos
+ unit
.topt(start
), xpos
, ypos
+ unit
.topt(end
))
3156 raise RuntimeError("invalid isometric direction '%s'" % direction
)
3158 def visometric_pt(self
, vx
, vy
, direction
, start_pt
, end_pt
):
3159 xpos
= self
.xpos_pt
+vx
*self
.width_pt
3160 ypos
= self
.ypos_pt
+vy
*self
.height_pt
3161 if direction
== "x":
3162 return path
.line_pt(xpos
+ start_pt
, ypos
, xpos
+ end_pt
, ypos
)
3163 elif direction
== "y":
3164 return path
.line_pt(xpos
, ypos
+ start_pt
, xpos
, ypos
+ end_pt
)
3165 raise RuntimeError("invalid isometric direction '%s'" % direction
)
3167 def visometric(self
, vx
, vy
, direction
, start
, end
):
3168 xpos
= self
.xpos_pt
+vx
*self
.width_pt
3169 ypos
= self
.ypos_pt
+vy
*self
.height_pt
3170 if direction
== "x":
3171 return path
.line_pt(xpos
+ unit
.topt(start
), ypos
, xpos
+ unit
.topt(end
), ypos
)
3172 elif direction
== "y":
3173 return path
.line_pt(xpos
, ypos
+ unit
.topt(start
), xpos
, ypos
+ unit
.topt(end
))
3174 raise RuntimeError("invalid isometric direction '%s'" % direction
)
3176 def keynum(self
, key
):
3178 while key
[0] in string
.letters
:
3184 def removedomethod(self
, method
):
3188 self
.domethods
.remove(method
)
3194 if not self
.removedomethod(self
.dolayout
): return
3196 # count the usage of styles and perform selects
3198 for data
in self
.plotdata
:
3200 styletotal
[id(data
.style
)] += 1
3202 styletotal
[id(data
.style
)] = 1
3204 for data
in self
.plotdata
:
3206 styleindex
[id(data
.style
)] += 1
3208 styleindex
[id(data
.style
)] = 0
3209 data
.selectstyle(self
, styleindex
[id(data
.style
)], styletotal
[id(data
.style
)])
3211 # adjust the axes ranges
3212 for step
in range(3):
3213 for data
in self
.plotdata
:
3214 data
.adjustaxes(self
, step
)
3217 axesdist
= unit
.length(self
.axesdist_str
, default_type
="v")
3218 XPattern
= re
.compile(r
"%s([2-9]|[1-9][0-9]+)?$" % self
.axisnames
[0])
3219 YPattern
= re
.compile(r
"%s([2-9]|[1-9][0-9]+)?$" % self
.axisnames
[1])
3220 xaxisextents
= [0, 0]
3221 yaxisextents
= [0, 0]
3222 needxaxisdist
= [0, 0]
3223 needyaxisdist
= [0, 0]
3224 items
= list(self
.axes
.items())
3225 items
.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3226 for key
, axis
in items
:
3227 num
= self
.keynum(key
)
3228 num2
= 1 - num
% 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3229 num3
= 2 * (num
% 2) - 1 # x1 -> 1, x2 -> -1, x3 -> 1, x4 -> -1, ...
3230 if XPattern
.match(key
):
3231 if needxaxisdist
[num2
]:
3232 xaxisextents
[num2
] += axesdist
3233 self
.axespos
[key
] = lineaxisposlinegrid(self
.axes
[key
].convert
,
3235 self
.ypos
+ num2
*self
.height
- num3
*xaxisextents
[num2
],
3236 self
.xpos
+ self
.width
,
3237 self
.ypos
+ num2
*self
.height
- num3
*xaxisextents
[num2
],
3239 xaxisextents
[num2
], xaxisextents
[num2
] + self
.height
)
3241 self
.xbasepath
= self
.axespos
[key
].basepath
3242 self
.xvbasepath
= self
.axespos
[key
].vbasepath
3243 self
.xgridpath
= self
.axespos
[key
].gridpath
3244 self
.xvgridpath
= self
.axespos
[key
].vgridpath
3245 self
.xtickpoint_pt
= self
.axespos
[key
].tickpoint_pt
3246 self
.xtickpoint
= self
.axespos
[key
].tickpoint
3247 self
.xvtickpoint_pt
= self
.axespos
[key
].vtickpoint_pt
3248 self
.xvtickpoint
= self
.axespos
[key
].tickpoint
3249 self
.xtickdirection
= self
.axespos
[key
].tickdirection
3250 self
.xvtickdirection
= self
.axespos
[key
].vtickdirection
3251 elif YPattern
.match(key
):
3252 if needyaxisdist
[num2
]:
3253 yaxisextents
[num2
] += axesdist
3254 self
.axespos
[key
] = lineaxisposlinegrid(self
.axes
[key
].convert
,
3255 self
.xpos
+ num2
*self
.width
- num3
*yaxisextents
[num2
],
3257 self
.xpos
+ num2
*self
.width
- num3
*yaxisextents
[num2
],
3258 self
.ypos
+ self
.height
,
3260 yaxisextents
[num2
], yaxisextents
[num2
] + self
.width
)
3262 self
.ybasepath
= self
.axespos
[key
].basepath
3263 self
.yvbasepath
= self
.axespos
[key
].vbasepath
3264 self
.ygridpath
= self
.axespos
[key
].gridpath
3265 self
.yvgridpath
= self
.axespos
[key
].vgridpath
3266 self
.ytickpoint_pt
= self
.axespos
[key
].tickpoint_pt
3267 self
.ytickpoint
= self
.axespos
[key
].tickpoint
3268 self
.yvtickpoint_pt
= self
.axespos
[key
].vtickpoint_pt
3269 self
.yvtickpoint
= self
.axespos
[key
].tickpoint
3270 self
.ytickdirection
= self
.axespos
[key
].tickdirection
3271 self
.yvtickdirection
= self
.axespos
[key
].vtickdirection
3273 raise ValueError("Axis key '%s' not allowed" % key
)
3274 axis
.finish(self
.axespos
[key
])
3275 if XPattern
.match(key
):
3276 xaxisextents
[num2
] += axis
.axiscanvas
.extent
3277 needxaxisdist
[num2
] = 1
3278 if YPattern
.match(key
):
3279 yaxisextents
[num2
] += axis
.axiscanvas
.extent
3280 needyaxisdist
[num2
] = 1
3283 def dobackground(self
):
3285 if not self
.removedomethod(self
.dobackground
): return
3286 if self
.backgroundattrs
is not None:
3287 self
.draw(path
.rect_pt(self
.xpos_pt
, self
.ypos_pt
, self
.width_pt
, self
.height_pt
),
3288 helper
.ensurelist(self
.backgroundattrs
))
3292 if not self
.removedomethod(self
.doaxes
): return
3293 for axis
in self
.axes
.values():
3294 self
.insert(axis
.axiscanvas
)
3298 if not self
.removedomethod(self
.dodata
): return
3299 for data
in self
.plotdata
:
3302 def _dokey(self
, key
, *plotinfos
):
3303 key
.setplotinfos(*plotinfos
)
3308 x
= self
.xpos_pt
+ self
.width_pt
- bbox
.urx
- key
.hdist_pt
3310 x
= self
.xpos_pt
+ self
.width_pt
- bbox
.llx
+ key
.hdist_pt
3313 x
= self
.xpos_pt
- bbox
.llx
+ key
.hdist_pt
3315 x
= self
.xpos_pt
- bbox
.urx
- key
.hdist_pt
3318 y
= self
.ypos_pt
+ self
.height_pt
- bbox
.ury
- key
.vdist_pt
3320 y
= self
.ypos_pt
+ self
.height_pt
- bbox
.lly
+ key
.vdist_pt
3323 y
= self
.ypos_pt
- bbox
.lly
+ key
.vdist_pt
3325 y
= self
.ypos_pt
- bbox
.ury
- key
.vdist_pt
3326 key
.paint(self
, x
, y
)
3330 if not self
.removedomethod(self
.dokey
): return
3331 if self
.key
is not None:
3332 self
._dokey
(self
.key
, *self
.plotinfos
)
3333 for key
, plotinfos
in self
.addkeys
:
3334 self
._dokey
(key
, *plotinfos
)
3337 while len(self
.domethods
):
3340 def initwidthheight(self
, width
, height
, ratio
):
3341 if (width
is not None) and (height
is None):
3342 self
.width
= unit
.length(width
)
3343 self
.height
= (1.0/ratio
) * self
.width
3344 elif (height
is not None) and (width
is None):
3345 self
.height
= unit
.length(height
)
3346 self
.width
= ratio
* self
.height
3348 self
.width
= unit
.length(width
)
3349 self
.height
= unit
.length(height
)
3350 self
.width_pt
= unit
.topt(self
.width
)
3351 self
.height_pt
= unit
.topt(self
.height
)
3352 if self
.width_pt
<= 0: raise ValueError("width <= 0")
3353 if self
.height_pt
<= 0: raise ValueError("height <= 0")
3355 def initaxes(self
, axes
, addlinkaxes
=0):
3356 for key
in self
.axisnames
:
3357 if not axes
.has_key(key
):
3358 axes
[key
] = linaxis()
3359 elif axes
[key
] is None:
3362 if not axes
.has_key(key
+ "2") and axes
.has_key(key
):
3363 axes
[key
+ "2"] = axes
[key
].createlinkaxis()
3364 elif axes
[key
+ "2"] is None:
3368 def __init__(self
, xpos
=0, ypos
=0, width
=None, height
=None, ratio
=goldenmean
,
3369 key
=None, backgroundattrs
=None, axesdist
="0.8 cm", **axes
):
3370 canvas
.canvas
.__init
__(self
)
3371 self
.xpos
= unit
.length(xpos
)
3372 self
.ypos
= unit
.length(ypos
)
3373 self
.xpos_pt
= unit
.topt(self
.xpos
)
3374 self
.ypos_pt
= unit
.topt(self
.ypos
)
3375 self
.initwidthheight(width
, height
, ratio
)
3376 self
.initaxes(axes
, 1)
3377 self
.axescanvas
= {}
3380 self
.backgroundattrs
= backgroundattrs
3381 self
.axesdist_str
= axesdist
3383 self
.domethods
= [self
.dolayout
, self
.dobackground
, self
.doaxes
, self
.dodata
, self
.dokey
]
3389 return canvas
.canvas
.bbox(self
)
3391 def write(self
, file):
3393 canvas
.canvas
.write(self
, file)
3397 # some thoughts, but deferred right now
3399 # class graphxyz(graphxy):
3401 # axisnames = "x", "y", "z"
3403 # def _vxtickpoint(self, axis, v):
3404 # return self._vpos(v, axis.vypos, axis.vzpos)
3406 # def _vytickpoint(self, axis, v):
3407 # return self._vpos(axis.vxpos, v, axis.vzpos)
3409 # def _vztickpoint(self, axis, v):
3410 # return self._vpos(axis.vxpos, axis.vypos, v)
3412 # def vxtickdirection(self, axis, v):
3413 # x1, y1 = self._vpos(v, axis.vypos, axis.vzpos)
3414 # x2, y2 = self._vpos(v, 0.5, 0)
3415 # dx, dy = x1 - x2, y1 - y2
3416 # norm = math.sqrt(dx*dx + dy*dy)
3417 # return dx/norm, dy/norm
3419 # def vytickdirection(self, axis, v):
3420 # x1, y1 = self._vpos(axis.vxpos, v, axis.vzpos)
3421 # x2, y2 = self._vpos(0.5, v, 0)
3422 # dx, dy = x1 - x2, y1 - y2
3423 # norm = math.sqrt(dx*dx + dy*dy)
3424 # return dx/norm, dy/norm
3426 # def vztickdirection(self, axis, v):
3428 # x1, y1 = self._vpos(axis.vxpos, axis.vypos, v)
3429 # x2, y2 = self._vpos(0.5, 0.5, v)
3430 # dx, dy = x1 - x2, y1 - y2
3431 # norm = math.sqrt(dx*dx + dy*dy)
3432 # return dx/norm, dy/norm
3434 # def _pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3435 # if xaxis is None: xaxis = self.axes["x"]
3436 # if yaxis is None: yaxis = self.axes["y"]
3437 # if zaxis is None: zaxis = self.axes["z"]
3438 # return self._vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3440 # def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3441 # if xaxis is None: xaxis = self.axes["x"]
3442 # if yaxis is None: yaxis = self.axes["y"]
3443 # if zaxis is None: zaxis = self.axes["z"]
3444 # return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3446 # def _vpos(self, vx, vy, vz):
3447 # x, y, z = (vx - 0.5)*self._depth, (vy - 0.5)*self._width, (vz - 0.5)*self._height
3448 # d0 = float(self.a[0]*self.b[1]*(z-self.eye[2])
3449 # + self.a[2]*self.b[0]*(y-self.eye[1])
3450 # + self.a[1]*self.b[2]*(x-self.eye[0])
3451 # - self.a[2]*self.b[1]*(x-self.eye[0])
3452 # - self.a[0]*self.b[2]*(y-self.eye[1])
3453 # - self.a[1]*self.b[0]*(z-self.eye[2]))
3454 # da = (self.eye[0]*self.b[1]*(z-self.eye[2])
3455 # + self.eye[2]*self.b[0]*(y-self.eye[1])
3456 # + self.eye[1]*self.b[2]*(x-self.eye[0])
3457 # - self.eye[2]*self.b[1]*(x-self.eye[0])
3458 # - self.eye[0]*self.b[2]*(y-self.eye[1])
3459 # - self.eye[1]*self.b[0]*(z-self.eye[2]))
3460 # db = (self.a[0]*self.eye[1]*(z-self.eye[2])
3461 # + self.a[2]*self.eye[0]*(y-self.eye[1])
3462 # + self.a[1]*self.eye[2]*(x-self.eye[0])
3463 # - self.a[2]*self.eye[1]*(x-self.eye[0])
3464 # - self.a[0]*self.eye[2]*(y-self.eye[1])
3465 # - self.a[1]*self.eye[0]*(z-self.eye[2]))
3466 # return da/d0 + self._xpos, db/d0 + self._ypos
3468 # def vpos(self, vx, vy, vz):
3469 # tx, ty = self._vpos(vx, vy, vz)
3470 # return unit.t_pt(tx), unit.t_pt(ty)
3472 # def xbaseline(self, axis, x1, x2, xaxis=None):
3473 # if xaxis is None: xaxis = self.axes["x"]
3474 # return self.vxbaseline(axis, xaxis.convert(x1), xaxis.convert(x2))
3476 # def ybaseline(self, axis, y1, y2, yaxis=None):
3477 # if yaxis is None: yaxis = self.axes["y"]
3478 # return self.vybaseline(axis, yaxis.convert(y1), yaxis.convert(y2))
3480 # def zbaseline(self, axis, z1, z2, zaxis=None):
3481 # if zaxis is None: zaxis = self.axes["z"]
3482 # return self.vzbaseline(axis, zaxis.convert(z1), zaxis.convert(z2))
3484 # def vxbaseline(self, axis, v1, v2):
3485 # return (path._line(*(self._vpos(v1, 0, 0) + self._vpos(v2, 0, 0))) +
3486 # path._line(*(self._vpos(v1, 0, 1) + self._vpos(v2, 0, 1))) +
3487 # path._line(*(self._vpos(v1, 1, 1) + self._vpos(v2, 1, 1))) +
3488 # path._line(*(self._vpos(v1, 1, 0) + self._vpos(v2, 1, 0))))
3490 # def vybaseline(self, axis, v1, v2):
3491 # return (path._line(*(self._vpos(0, v1, 0) + self._vpos(0, v2, 0))) +
3492 # path._line(*(self._vpos(0, v1, 1) + self._vpos(0, v2, 1))) +
3493 # path._line(*(self._vpos(1, v1, 1) + self._vpos(1, v2, 1))) +
3494 # path._line(*(self._vpos(1, v1, 0) + self._vpos(1, v2, 0))))
3496 # def vzbaseline(self, axis, v1, v2):
3497 # return (path._line(*(self._vpos(0, 0, v1) + self._vpos(0, 0, v2))) +
3498 # path._line(*(self._vpos(0, 1, v1) + self._vpos(0, 1, v2))) +
3499 # path._line(*(self._vpos(1, 1, v1) + self._vpos(1, 1, v2))) +
3500 # path._line(*(self._vpos(1, 0, v1) + self._vpos(1, 0, v2))))
3502 # def xgridpath(self, x, xaxis=None):
3504 # if xaxis is None: xaxis = self.axes["x"]
3505 # v = xaxis.convert(x)
3506 # return path._line(self._xpos+v*self._width, self._ypos,
3507 # self._xpos+v*self._width, self._ypos+self._height)
3509 # def ygridpath(self, y, yaxis=None):
3511 # if yaxis is None: yaxis = self.axes["y"]
3512 # v = yaxis.convert(y)
3513 # return path._line(self._xpos, self._ypos+v*self._height,
3514 # self._xpos+self._width, self._ypos+v*self._height)
3516 # def zgridpath(self, z, zaxis=None):
3518 # if zaxis is None: zaxis = self.axes["z"]
3519 # v = zaxis.convert(z)
3520 # return path._line(self._xpos, self._zpos+v*self._height,
3521 # self._xpos+self._width, self._zpos+v*self._height)
3523 # def vxgridpath(self, v):
3524 # return path.path(path._moveto(*self._vpos(v, 0, 0)),
3525 # path._lineto(*self._vpos(v, 0, 1)),
3526 # path._lineto(*self._vpos(v, 1, 1)),
3527 # path._lineto(*self._vpos(v, 1, 0)),
3530 # def vygridpath(self, v):
3531 # return path.path(path._moveto(*self._vpos(0, v, 0)),
3532 # path._lineto(*self._vpos(0, v, 1)),
3533 # path._lineto(*self._vpos(1, v, 1)),
3534 # path._lineto(*self._vpos(1, v, 0)),
3537 # def vzgridpath(self, v):
3538 # return path.path(path._moveto(*self._vpos(0, 0, v)),
3539 # path._lineto(*self._vpos(0, 1, v)),
3540 # path._lineto(*self._vpos(1, 1, v)),
3541 # path._lineto(*self._vpos(1, 0, v)),
3544 # def _addpos(self, x, y, dx, dy):
3548 # def _connect(self, x1, y1, x2, y2):
3550 # return path._lineto(x2, y2)
3554 # if not self.removedomethod(self.doaxes): return
3555 # axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
3556 # XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.axisnames[0])
3557 # YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.axisnames[1])
3558 # ZPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.axisnames[2])
3559 # items = list(self.axes.items())
3560 # items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3561 # for key, axis in items:
3562 # num = self.keynum(key)
3563 # num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3564 # num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
3565 # if XPattern.match(key):
3568 # axis._vtickpoint = self._vxtickpoint
3569 # axis.vgridpath = self.vxgridpath
3570 # axis.vbaseline = self.vxbaseline
3571 # axis.vtickdirection = self.vxtickdirection
3572 # elif YPattern.match(key):
3575 # axis._vtickpoint = self._vytickpoint
3576 # axis.vgridpath = self.vygridpath
3577 # axis.vbaseline = self.vybaseline
3578 # axis.vtickdirection = self.vytickdirection
3579 # elif ZPattern.match(key):
3582 # axis._vtickpoint = self._vztickpoint
3583 # axis.vgridpath = self.vzgridpath
3584 # axis.vbaseline = self.vzbaseline
3585 # axis.vtickdirection = self.vztickdirection
3587 # raise ValueError("Axis key '%s' not allowed" % key)
3588 # if axis.painter is not None:
3589 # axis.dopaint(self)
3590 # # if XPattern.match(key):
3591 # # self._xaxisextents[num2] += axis._extent
3592 # # needxaxisdist[num2] = 1
3593 # # if YPattern.match(key):
3594 # # self._yaxisextents[num2] += axis._extent
3595 # # needyaxisdist[num2] = 1
3597 # def __init__(self, tex, xpos=0, ypos=0, width=None, height=None, depth=None,
3598 # phi=30, theta=30, distance=1,
3599 # backgroundattrs=None, axesdist="0.8 cm", **axes):
3600 # canvas.canvas.__init__(self)
3604 # self._xpos = unit.topt(xpos)
3605 # self._ypos = unit.topt(ypos)
3606 # self._width = unit.topt(width)
3607 # self._height = unit.topt(height)
3608 # self._depth = unit.topt(depth)
3609 # self.width = width
3610 # self.height = height
3611 # self.depth = depth
3612 # if self._width <= 0: raise ValueError("width < 0")
3613 # if self._height <= 0: raise ValueError("height < 0")
3614 # if self._depth <= 0: raise ValueError("height < 0")
3615 # self._distance = distance*math.sqrt(self._width*self._width+
3616 # self._height*self._height+
3617 # self._depth*self._depth)
3618 # phi *= -math.pi/180
3619 # theta *= math.pi/180
3620 # self.a = (-math.sin(phi), math.cos(phi), 0)
3621 # self.b = (-math.cos(phi)*math.sin(theta),
3622 # -math.sin(phi)*math.sin(theta),
3624 # self.eye = (self._distance*math.cos(phi)*math.cos(theta),
3625 # self._distance*math.sin(phi)*math.cos(theta),
3626 # self._distance*math.sin(theta))
3627 # self.initaxes(axes)
3628 # self.axesdist_str = axesdist
3629 # self.backgroundattrs = backgroundattrs
3632 # self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata]
3633 # self.haslayout = 0
3634 # self.defaultstyle = {}
3638 # return bbox._bbox(self._xpos - 200, self._ypos - 200, self._xpos + 200, self._ypos + 200)
3641 ################################################################################
3643 ################################################################################
3648 def cross(self
, x_pt
, y_pt
, size_pt
):
3649 return (path
.moveto_pt(x_pt
-0.5*size_pt
, y_pt
-0.5*size_pt
),
3650 path
.lineto_pt(x_pt
+0.5*size_pt
, y_pt
+0.5*size_pt
),
3651 path
.moveto_pt(x_pt
-0.5*size_pt
, y_pt
+0.5*size_pt
),
3652 path
.lineto_pt(x_pt
+0.5*size_pt
, y_pt
-0.5*size_pt
))
3654 def plus(self
, x_pt
, y_pt
, size_pt
):
3655 return (path
.moveto_pt(x_pt
-0.707106781*size_pt
, y_pt
),
3656 path
.lineto_pt(x_pt
+0.707106781*size_pt
, y_pt
),
3657 path
.moveto_pt(x_pt
, y_pt
-0.707106781*size_pt
),
3658 path
.lineto_pt(x_pt
, y_pt
+0.707106781*size_pt
))
3660 def square(self
, x_pt
, y_pt
, size_pt
):
3661 return (path
.moveto_pt(x_pt
-0.5*size_pt
, y_pt
-0.5 * size_pt
),
3662 path
.lineto_pt(x_pt
+0.5*size_pt
, y_pt
-0.5 * size_pt
),
3663 path
.lineto_pt(x_pt
+0.5*size_pt
, y_pt
+0.5 * size_pt
),
3664 path
.lineto_pt(x_pt
-0.5*size_pt
, y_pt
+0.5 * size_pt
),
3667 def triangle(self
, x_pt
, y_pt
, size_pt
):
3668 return (path
.moveto_pt(x_pt
-0.759835685*size_pt
, y_pt
-0.438691337*size_pt
),
3669 path
.lineto_pt(x_pt
+0.759835685*size_pt
, y_pt
-0.438691337*size_pt
),
3670 path
.lineto_pt(x_pt
, y_pt
+0.877382675*size_pt
),
3673 def circle(self
, x_pt
, y_pt
, size_pt
):
3674 return (path
.arc_pt(x_pt
, y_pt
, 0.564189583*size_pt
, 0, 360),
3677 def diamond(self
, x_pt
, y_pt
, size_pt
):
3678 return (path
.moveto_pt(x_pt
-0.537284965*size_pt
, y_pt
),
3679 path
.lineto_pt(x_pt
, y_pt
-0.930604859*size_pt
),
3680 path
.lineto_pt(x_pt
+0.537284965*size_pt
, y_pt
),
3681 path
.lineto_pt(x_pt
, y_pt
+0.930604859*size_pt
),
3684 changecross
= attr
.changelist([cross
, plus
, square
, triangle
, circle
, diamond
])
3685 changeplus
= attr
.changelist([plus
, square
, triangle
, circle
, diamond
, cross
])
3686 changesquare
= attr
.changelist([square
, triangle
, circle
, diamond
, cross
, plus
])
3687 changetriangle
= attr
.changelist([triangle
, circle
, diamond
, cross
, plus
, square
])
3688 changecircle
= attr
.changelist([circle
, diamond
, cross
, plus
, square
, triangle
])
3689 changediamond
= attr
.changelist([diamond
, cross
, plus
, square
, triangle
, circle
])
3690 changesquaretwice
= attr
.changelist([square
, square
, triangle
, triangle
, circle
, circle
, diamond
, diamond
])
3691 changetriangletwice
= attr
.changelist([triangle
, triangle
, circle
, circle
, diamond
, diamond
, square
, square
])
3692 changecircletwice
= attr
.changelist([circle
, circle
, diamond
, diamond
, square
, square
, triangle
, triangle
])
3693 changediamondtwice
= attr
.changelist([diamond
, diamond
, square
, square
, triangle
, triangle
, circle
, circle
])
3695 changestrokedfilled
= attr
.changelist([deco
.stroked
, deco
.filled
])
3696 changefilledstroked
= attr
.changelist([deco
.filled
, deco
.stroked
])
3698 changelinestyle
= attr
.changelist([style
.linestyle
.solid
,
3699 style
.linestyle
.dashed
,
3700 style
.linestyle
.dotted
,
3701 style
.linestyle
.dashdotted
])
3703 defaultsymbolattrs
= [deco
.stroked
]
3704 defaulterrorbarattrs
= []
3705 defaultlineattrs
= [changelinestyle
]
3707 def __init__(self
, symbol
=changecross
,
3713 self
.size_str
= size
3714 self
.symbol
= symbol
3715 self
.errorscale
= errorscale
3716 self
.symbolattrs
= symbolattrs
3717 self
.errorbarattrs
= errorbarattrs
3718 self
.lineattrs
= lineattrs
3720 def setdata(self
, graph
, columns
, selectindex
, selecttotal
, data
):
3722 - the instance should be considered read-only
3723 (it might be shared between several data)
3724 - data is the place where to store information
3725 - returns the dictionary of columns not used by the style"""
3728 data
.symbol
= attr
.selectattr(self
.symbol
, selectindex
, selecttotal
)
3729 data
.size_pt
= unit
.topt(unit
.length(attr
.selectattr(self
.size_str
, selectindex
, selecttotal
), default_type
="v"))
3730 data
.errorsize_pt
= self
.errorscale
* data
.size_pt
3731 if self
.symbolattrs
is not None:
3732 data
.symbolattrs
= attr
.selectattrs(self
.defaultsymbolattrs
+ self
.symbolattrs
, selectindex
, selecttotal
)
3734 data
.symbolattrs
= None
3735 if self
.errorbarattrs
is not None:
3736 data
.errorbarattrs
= attr
.selectattrs(self
.defaulterrorbarattrs
+ self
.errorbarattrs
, selectindex
, selecttotal
)
3738 data
.errorbarattrs
= None
3739 if self
.lineattrs
is not None:
3740 data
.lineattrs
= attr
.selectattrs(self
.defaultlineattrs
+ self
.lineattrs
, selectindex
, selecttotal
)
3742 data
.lineattrs
= None
3744 # analyse column information
3745 data
.index
= {} # a nested index dictionary containing
3746 # column numbers, e.g. data.index["x"]["x"],
3747 # data.index["y"]["dmin"] etc.; the first key is a axis
3748 # name (without the axis number), the second is one of
3749 # the datanames ["x", "min", "max", "d", "dmin", "dmax"]
3750 data
.axes
= {} # mapping from axis name (without axis number) to the axis
3752 for axisname
in graph
.axisnames
:
3754 for dataname
, pattern
in [("x", re
.compile(r
"(%s([2-9]|[1-9][0-9]+)?)$" % axisname
)),
3755 ("min", re
.compile(r
"(%s([2-9]|[1-9][0-9]+)?)min$" % axisname
)),
3756 ("max", re
.compile(r
"(%s([2-9]|[1-9][0-9]+)?)max$" % axisname
)),
3757 ("d", re
.compile(r
"d(%s([2-9]|[1-9][0-9]+)?)$" % axisname
)),
3758 ("dmin", re
.compile(r
"d(%s([2-9]|[1-9][0-9]+)?)min$" % axisname
)),
3759 ("dmax", re
.compile(r
"d(%s([2-9]|[1-9][0-9]+)?)max$" % axisname
))]:
3760 for datakey
, index
in columns
.items():
3761 match
= pattern
.match(datakey
)
3764 axiskey
= match
.groups()[0]
3765 data
.index
[axisname
] = {dataname
: index
}
3766 data
.axes
[axisname
] = graph
.axes
[axiskey
]
3767 elif axiskey
== match
.groups()[0]:
3768 data
.index
[axisname
][dataname
] = index
3770 raise ValueError("axis key mismatch for axis name '%s'" % axisname
)
3771 if datakey
in useddatakeys
:
3772 raise RuntimeError("multiple datakey matching")
3773 useddatakeys
.append(datakey
)
3775 raise ValueError("missing columns for axis name '%s'" % axisname
)
3776 if ((data
.index
[axisname
].has_key("min") and data
.index
[axisname
].has_key("d")) or
3777 (data
.index
[axisname
].has_key("min") and data
.index
[axisname
].has_key("dmin")) or
3778 (data
.index
[axisname
].has_key("d") and data
.index
[axisname
].has_key("dmin")) or
3779 (data
.index
[axisname
].has_key("max") and data
.index
[axisname
].has_key("d")) or
3780 (data
.index
[axisname
].has_key("max") and data
.index
[axisname
].has_key("dmax")) or
3781 (data
.index
[axisname
].has_key("d") and data
.index
[axisname
].has_key("dmax"))):
3782 raise ValueError("multiple errorbar definition for axis name '%s'" % axisname
)
3783 if (not data
.index
[axisname
].has_key("x") and
3784 (data
.index
[axisname
].has_key("d") or
3785 data
.index
[axisname
].has_key("dmin") or
3786 data
.index
[axisname
].has_key("dmax"))):
3787 raise ValueError("errorbar definition start value missing for axis name '%s'" % axisname
)
3789 # return unused column information
3791 for key
, value
in columns
.items():
3792 if key
not in useddatakeys
:
3796 def adjustaxes(self
, axisnames
, data
):
3797 for axisname
in axisnames
:
3798 if data
.index
[axisname
].has_key("x"):
3799 data
.axes
[axisname
].adjustrange(data
.points
, data
.index
[axisname
]["x"])
3800 if data
.index
[axisname
].has_key("min"):
3801 data
.axes
[axisname
].adjustrange(data
.points
, data
.index
[axisname
]["min"])
3802 if data
.index
[axisname
].has_key("max"):
3803 data
.axes
[axisname
].adjustrange(data
.points
, data
.index
[axisname
]["max"])
3804 if data
.index
[axisname
].has_key("d"):
3805 data
.axes
[axisname
].adjustrange(data
.points
, data
.index
[axisname
]["x"], deltaindex
=data
.index
[axisname
]["d"])
3806 if data
.index
[axisname
].has_key("dmin"):
3807 data
.axes
[axisname
].adjustrange(data
.points
, data
.index
[axisname
]["x"], deltaminindex
=data
.index
[axisname
]["dmin"])
3808 if data
.index
[axisname
].has_key("dmax"):
3809 data
.axes
[axisname
].adjustrange(data
.points
, data
.index
[axisname
]["x"], deltamaxindex
=data
.index
[axisname
]["dmax"])
3811 def drawsymbol_pt(self
, c
, x_pt
, y_pt
, data
, point
=None):
3812 if data
.symbolattrs
is not None:
3813 c
.draw(path
.path(*data
.symbol(self
, x_pt
, y_pt
, data
.size_pt
)), data
.symbolattrs
)
3815 def key_pt(self
, c
, x_pt
, y_pt
, width_pt
, height_pt
, data
):
3816 self
.drawsymbol_pt(c
, x_pt
+0.5*width
, y_pt
+0.5*height
, data
)
3817 if data
.lineattrs
is not None:
3818 c
.stroke(path
.line_pt(x_pt
, y_pt
+0.5*height
, x_pt
+width
, y_pt
+0.5*height
), data
.lineattrs
)
3820 def drawpoints(self
, graph
, data
):
3821 if data
.lineattrs
is not None or data
.errorbarattrs
is not None:
3822 clipcanvas
= graph
.clipcanvas()
3823 data
.line
= path
.path()
3827 for axisname
in graph
.axisnames
:
3828 axesdict
[axisname
+ "axis"] = data
.axes
[axisname
]
3829 ranges
.append(data
.axes
[axisname
].getrange())
3831 if data
.errorbarattrs
is not None:
3832 for axisname
in graph
.axisnames
:
3833 if data
.index
[axisname
].keys() != ["x"]:
3834 errornames
.append(axisname
)
3836 for point
in data
.points
:
3839 pos
= [point
[data
.index
[axisname
]["x"]] for axisname
in graph
.axisnames
]
3840 xpos
, ypos
= graph
.pos_pt(*pos
, **axesdict
)
3844 for (min, max), value
in zip(ranges
, pos
):
3845 if value
< min or value
> max:
3848 self
.drawsymbol_pt(graph
, xpos
, ypos
, data
, point
=point
)
3850 data
.line
.append(path
.moveto_pt(xpos
, ypos
))
3853 data
.line
.append(path
.lineto_pt(xpos
, ypos
))
3855 # errorbar loop over the different direction having errorbars
3856 for errorname
in errornames
:
3857 errorindex
= data
.index
[errorname
]
3859 # calculate min and max
3862 min = point
[errorindex
["x"]] - point
[errorindex
["d"]]
3866 max = point
[errorindex
["x"]] + point
[errorindex
["d"]]
3870 min = point
[errorindex
["x"]] - point
[errorindex
["dmin"]]
3874 max = point
[errorindex
["x"]] + point
[errorindex
["dmax"]]
3877 if errorindex
.has_key("min"):
3878 min = point
[errorindex
["min"]]
3879 if errorindex
.has_key("max"):
3880 max = point
[errorindex
["max"]]
3882 # create minpos and maxpos
3885 for axisname
in graph
.axisnames
:
3886 if axisname
!= errorname
:
3887 minpos
.append(point
[data
.index
[axisname
]["x"]])
3894 for axisname
in graph
.axisnames
:
3895 if axisname
!= errorname
:
3896 maxpos
.append(point
[data
.index
[axisname
]["x"]])
3902 # create path for errorbars
3903 errorpath
= path
.path()
3904 for frompos
, topos
in [(minpos
, maxpos
), (minpos
, pos
), (pos
, maxpos
)]:
3906 errorpath
+= graph
.geodesic(*(frompos
+ topos
), **axesdict
)
3911 caps
= [frompos
, topos
]
3920 for axisname
in graph
.axisnames
:
3921 if axisname
!= errorname
:
3922 errorpath
+= graph
.isometric_pt(*(cap
+ [axisname
, -data
.errorsize_pt
, data
.errorsize_pt
]), **axesdict
)
3926 if len(errorpath
.path
):
3927 clipcanvas
.stroke(errorpath
, data
.errorbarattrs
)
3929 if data
.lineattrs
is not None:
3930 clipcanvas
.stroke(data
.line
, data
.lineattrs
)
3933 class line(symbolline
):
3935 def __init__(self
, lineattrs
=[]):
3936 symbolline
.__init
__(self
, symbolattrs
=None, errorbarattrs
=None, lineattrs
=lineattrs
)
3939 class symbol(symbolline
):
3941 def __init__(self
, **kwargs
):
3942 symbolline
.__init
__(self
, lineattrs
=None, **kwargs
)
3946 # class rect(symbol):
3948 # def __init__(self, palette=color.palette.Gray):
3949 # self.palette = palette
3950 # self.colorindex = None
3951 # symbol.__init__(self, symbolattrs=None, errorbarattrs=(), lineattrs=None)
3953 # def iterate(self):
3954 # raise RuntimeError("style is not iterateable")
3956 # def othercolumnkey(self, key, index):
3957 # if key == "color":
3958 # self.colorindex = index
3960 # symbol.othercolumnkey(self, key, index)
3962 # def drawerrorbar_pt(self, graph, topleft, top, topright,
3963 # left, center, right,
3964 # bottomleft, bottom, bottomright, point=None):
3965 # color = point[self.colorindex]
3966 # if color is not None:
3967 # if color != self.lastcolor:
3968 # self.rectclipcanvas.set([self.palette.getcolor(color)])
3969 # if bottom is not None and left is not None:
3970 # bottomleft = left[0], bottom[1]
3971 # if bottom is not None and right is not None:
3972 # bottomright = right[0], bottom[1]
3973 # if top is not None and right is not None:
3974 # topright = right[0], top[1]
3975 # if top is not None and left is not None:
3976 # topleft = left[0], top[1]
3977 # if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
3978 # self.rectclipcanvas.fill(path.path(path.moveto_pt(*bottomleft),
3979 # graph._connect(*(bottomleft+bottomright)),
3980 # graph._connect(*(bottomright+topright)),
3981 # graph._connect(*(topright+topleft)),
3982 # path.closepath()))
3984 # def drawpoints(self, graph, points):
3985 # if self.colorindex is None:
3986 # raise RuntimeError("column 'color' not set")
3987 # self.lastcolor = None
3988 # self.rectclipcanvas = graph.clipcanvas()
3989 # symbol.drawpoints(self, graph, points)
3991 # def key(self, c, x, y, width, height):
3992 # raise RuntimeError("style doesn't yet provide a key")
3997 defaulttextattrs
= [textmodule
.halign
.center
, textmodule
.vshift
.mathaxis
]
3999 def __init__(self
, textdx
="0", textdy
="0.3 cm", textattrs
=[], **kwargs
):
4000 self
.textdx_str
= textdx
4001 self
.textdy_str
= textdy
4002 self
.textattrs
= textattrs
4003 symbol
.__init
__(self
, **kwargs
)
4005 def setdata(self
, graph
, columns
, selectindex
, selecttotal
, data
):
4007 if self
.textattrs
is not None:
4008 data
.textattrs
= attr
.selectattrs(self
.defaulttextattrs
+ self
.textattrs
, selectindex
, selecttotal
)
4010 data
.textattrs
= None
4012 # analyse column information
4013 columns
= columns
.copy()
4014 data
.textindex
= columns
["text"]
4016 return symbol
.setdata(self
, graph
, columns
, selectindex
, selecttotal
, data
)
4018 def drawsymbol_pt(self
, c
, x
, y
, data
, point
=None):
4019 symbol
.drawsymbol_pt(self
, c
, x
, y
, data
, point
)
4020 if None not in (x
, y
, point
[data
.textindex
]) and data
.textattrs
is not None:
4021 c
.text_pt(x
+ data
.textdx_pt
, y
+ data
.textdy_pt
, str(point
[data
.textindex
]), data
.textattrs
)
4023 def drawpoints(self
, graph
, points
):
4024 data
.textdx
= unit
.length(self
.textdx_str
, default_type
="v")
4025 data
.textdy
= unit
.length(self
.textdy_str
, default_type
="v")
4026 data
.textdx_pt
= unit
.topt(data
.textdx
)
4027 data
.textdy_pt
= unit
.topt(data
.textdy
)
4028 symbol
.drawpoints(self
, graph
, points
)
4030 def key(self
, c
, x
, y
, width
, height
):
4031 raise RuntimeError("style doesn't yet provide a key")
4034 # class arrow(symbol):
4036 # def __init__(self, linelength="0.2 cm", arrowattrs=(), arrowsize="0.1 cm", arrowdict={}, epsilon=1e-10):
4037 # self.linelength_str = linelength
4038 # self.arrowsize_str = arrowsize
4039 # self.arrowattrs = arrowattrs
4040 # self.arrowdict = arrowdict
4041 # self.epsilon = epsilon
4042 # self.sizeindex = self.angleindex = None
4043 # symbol.__init__(self, symbolattrs=(), errorbarattrs=None, lineattrs=None)
4045 # def iterate(self):
4046 # raise RuntimeError("style is not iterateable")
4048 # def othercolumnkey(self, key, index):
4050 # self.sizeindex = index
4051 # elif key == "angle":
4052 # self.angleindex = index
4054 # symbol.othercolumnkey(self, key, index)
4056 # def drawsymbol_pt(self, graph, x, y, point=None):
4057 # if None not in (x, y, point[self.angleindex], point[self.sizeindex], self.arrowattrs, self.arrowdict):
4058 # if point[self.sizeindex] > self.epsilon:
4059 # dx, dy = math.cos(point[self.angleindex]*math.pi/180.0), math.sin(point[self.angleindex]*math.pi/180)
4060 # x1 = unit.t_pt(x)-0.5*dx*self.linelength*point[self.sizeindex]
4061 # y1 = unit.t_pt(y)-0.5*dy*self.linelength*point[self.sizeindex]
4062 # x2 = unit.t_pt(x)+0.5*dx*self.linelength*point[self.sizeindex]
4063 # y2 = unit.t_pt(y)+0.5*dy*self.linelength*point[self.sizeindex]
4064 # graph.stroke(path.line(x1, y1, x2, y2),
4065 # [deco.earrow(size=self.arrowsize*point[self.sizeindex],
4066 # **self.arrowdict)]+helper.ensurelist(self.arrowattrs))
4068 # def drawpoints(self, graph, points):
4069 # self.arrowsize = unit.length(_getattr(self.arrowsize_str), default_type="v")
4070 # self.linelength = unit.length(_getattr(self.linelength_str), default_type="v")
4071 # self.arrowsize_pt = unit.topt(self.arrowsize)
4072 # self.linelength_pt = unit.topt(self.linelength)
4073 # if self.sizeindex is None:
4074 # raise RuntimeError("column 'size' not set")
4075 # if self.angleindex is None:
4076 # raise RuntimeError("column 'angle' not set")
4077 # symbol.drawpoints(self, graph, points)
4079 # def key(self, c, x, y, width, height):
4080 # raise RuntimeError("style doesn't yet provide a key")
4083 # class _bariterator(attr.changeattr):
4085 # def attr(self, index):
4086 # return index, self.counter
4091 # def __init__(self, fromzero=1, stacked=0, skipmissing=1, xbar=0,
4092 # barattrs=helper.nodefault, _usebariterator=helper.nodefault, _previousbar=None):
4093 # self.fromzero = fromzero
4094 # self.stacked = stacked
4095 # self.skipmissing = skipmissing
4097 # if barattrs is helper.nodefault:
4098 # self._barattrs = [deco.stroked([color.gray.black]), changecolor.Rainbow()]
4100 # self._barattrs = barattrs
4101 # if _usebariterator is helper.nodefault:
4102 # self.bariterator = _bariterator()
4104 # self.bariterator = _usebariterator
4105 # self.previousbar = _previousbar
4107 # def iteratedict(self):
4109 # result["barattrs"] = _iterateattrs(self._barattrs)
4112 # def iterate(self):
4113 # return bar(fromzero=self.fromzero, stacked=self.stacked, xbar=self.xbar,
4114 # _usebariterator=_iterateattr(self.bariterator), _previousbar=self, **self.iteratedict())
4116 # def setcolumns(self, graph, columns):
4117 # def checkpattern(key, index, pattern, iskey, isindex):
4118 # if key is not None:
4119 # match = pattern.match(key)
4121 # if isindex is not None: raise ValueError("multiple key specification")
4122 # if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key axisnames")
4124 # iskey = match.groups()[0]
4126 # return key, iskey, isindex
4128 # xkey = ykey = None
4129 # if len(graph.axisnames) != 2: raise TypeError("style not applicable in graph")
4130 # XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[0])
4131 # YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[1])
4133 # for key, index in columns.items():
4134 # key, xkey, xi = checkpattern(key, index, XPattern, xkey, xi)
4135 # key, ykey, yi = checkpattern(key, index, YPattern, ykey, yi)
4136 # if key is not None:
4137 # self.othercolumnkey(key, index)
4138 # if None in (xkey, ykey): raise ValueError("incomplete axis specification")
4140 # self.nkey, self.ni = ykey, yi
4141 # self.vkey, self.vi = xkey, xi
4143 # self.nkey, self.ni = xkey, xi
4144 # self.vkey, self.vi = ykey, yi
4145 # self.naxis, self.vaxis = graph.axes[self.nkey], graph.axes[self.vkey]
4147 # def getranges(self, points):
4148 # index, count = _getattr(self.bariterator)
4149 # if count != 1 and self.stacked != 1:
4150 # if self.stacked > 1:
4151 # index = divmod(index, self.stacked)[0]
4153 # vmin = vmax = None
4154 # for point in points:
4155 # if not self.skipmissing:
4156 # if count != 1 and self.stacked != 1:
4157 # self.naxis.setname(point[self.ni], index)
4159 # self.naxis.setname(point[self.ni])
4161 # v = point[self.vi] + 0.0
4162 # if vmin is None or v < vmin: vmin = v
4163 # if vmax is None or v > vmax: vmax = v
4164 # except (TypeError, ValueError):
4167 # if self.skipmissing:
4168 # if count != 1 and self.stacked != 1:
4169 # self.naxis.setname(point[self.ni], index)
4171 # self.naxis.setname(point[self.ni])
4173 # if vmin > 0: vmin = 0
4174 # if vmax < 0: vmax = 0
4175 # return {self.vkey: (vmin, vmax)}
4177 # def drawpoints(self, graph, points):
4178 # index, count = _getattr(self.bariterator)
4179 # dostacked = (self.stacked != 0 and
4180 # (self.stacked == 1 or divmod(index, self.stacked)[1]) and
4181 # (self.stacked != 1 or index))
4182 # if self.stacked > 1:
4183 # index = divmod(index, self.stacked)[0]
4184 # vmin, vmax = self.vaxis.getrange()
4185 # self.barattrs = _getattrs(helper.ensuresequence(self._barattrs))
4187 # self.stackedvalue = {}
4188 # for point in points:
4190 # n = point[self.ni]
4191 # v = point[self.vi]
4193 # self.stackedvalue[n] = v
4194 # if count != 1 and self.stacked != 1:
4195 # minid = (n, index, 0)
4196 # maxid = (n, index, 1)
4201 # x1pos, y1pos = graph.pos_pt(v, minid, xaxis=self.vaxis, yaxis=self.naxis)
4202 # x2pos, y2pos = graph.pos_pt(v, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4204 # x1pos, y1pos = graph.pos_pt(minid, v, xaxis=self.naxis, yaxis=self.vaxis)
4205 # x2pos, y2pos = graph.pos_pt(maxid, v, xaxis=self.naxis, yaxis=self.vaxis)
4208 # x3pos, y3pos = graph.pos_pt(self.previousbar.stackedvalue[n], maxid, xaxis=self.vaxis, yaxis=self.naxis)
4209 # x4pos, y4pos = graph.pos_pt(self.previousbar.stackedvalue[n], minid, xaxis=self.vaxis, yaxis=self.naxis)
4211 # x3pos, y3pos = graph.pos_pt(maxid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4212 # x4pos, y4pos = graph.pos_pt(minid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4216 # x3pos, y3pos = graph.pos_pt(0, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4217 # x4pos, y4pos = graph.pos_pt(0, minid, xaxis=self.vaxis, yaxis=self.naxis)
4219 # x3pos, y3pos = graph.pos_pt(maxid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4220 # x4pos, y4pos = graph.pos_pt(minid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4222 # #x3pos, y3pos = graph.tickpoint_pt(maxid, axis=self.naxis)
4223 # #x4pos, y4pos = graph.tickpoint_pt(minid, axis=self.naxis)
4224 # x3pos, y3pos = graph.axespos[self.nkey].tickpoint_pt(maxid)
4225 # x4pos, y4pos = graph.axespos[self.nkey].tickpoint_pt(minid)
4226 # if self.barattrs is not None:
4227 # graph.fill(path.path(path.moveto_pt(x1pos, y1pos),
4228 # graph._connect(x1pos, y1pos, x2pos, y2pos),
4229 # graph._connect(x2pos, y2pos, x3pos, y3pos),
4230 # graph._connect(x3pos, y3pos, x4pos, y4pos),
4231 # graph._connect(x4pos, y4pos, x1pos, y1pos), # no closepath (might not be straight)
4232 # path.closepath()), self.barattrs)
4233 # except (TypeError, ValueError): pass
4235 # def key(self, c, x, y, width, height):
4236 # c.fill(path.rect_pt(x, y, width, height), self.barattrs)
4241 # # def setcolumns(self, graph, columns):
4242 # # self.columns = columns
4244 # # def getranges(self, points):
4245 # # return {"x": (0, 10), "y": (0, 10), "z": (0, 1)}
4247 # # def drawpoints(self, graph, points):
4252 ################################################################################
4254 ################################################################################
4259 defaultstyle
= symbol()
4261 def __init__(self
, file, title
=helper
.nodefault
, context
={}, **columns
):
4263 if helper
.isstring(file):
4264 self
.data
= datamodule
.datafile(file)
4267 if title
is helper
.nodefault
:
4268 self
.title
= "(unknown)"
4272 for key
, column
in columns
.items():
4274 self
.columns
[key
] = self
.data
.getcolumnno(column
)
4275 except datamodule
.ColumnError
:
4276 self
.columns
[key
] = len(self
.data
.titles
)
4277 self
.data
.addcolumn(column
, context
=context
)
4278 self
.points
= self
.data
.data
4280 def setstyle(self
, style
):
4283 def selectstyle(self
, graph
, selectindex
, selecttotal
):
4284 unhandledcolumns
= self
.style
.setdata(graph
, self
.columns
, selectindex
, selecttotal
, self
)
4285 unhandledcolumnkeys
= unhandledcolumns
.keys()
4286 if len(unhandledcolumnkeys
):
4287 raise ValueError("style couldn't handle column keys %s" % unhandledcolumnkeys
)
4289 def adjustaxes(self
, graph
, step
):
4291 - on step == 0 axes with fixed data should be adjusted
4292 - on step == 1 the current axes ranges might be used to
4293 calculate further data (e.g. y data for a function y=f(x)
4294 where the y range depends on the x range)
4295 - on step == 2 axes ranges not previously set should be
4296 updated by data accumulated by step 1"""
4298 self
.style
.adjustaxes(graph
.axisnames
, self
)
4300 def draw(self
, graph
):
4301 self
.style
.drawpoints(graph
, self
)
4306 defaultstyle
= line()
4308 def __init__(self
, expression
, title
=helper
.nodefault
, min=None, max=None, points
=100, parser
=mathtree
.parser(), context
={}):
4309 if title
is helper
.nodefault
:
4310 self
.title
= expression
4315 self
.nopoints
= points
4316 self
.context
= context
4317 self
.result
, expression
= [x
.strip() for x
in expression
.split("=")]
4318 self
.mathtree
= parser
.parse(expression
)
4319 self
.variable
= None
4321 def setstyle(self
, style
):
4324 def selectstyle(self
, graph
, selectindex
, selecttotal
):
4325 for variable
in self
.mathtree
.VarList():
4326 if variable
in graph
.axes
.keys():
4327 if self
.variable
is None:
4328 self
.variable
= variable
4330 raise ValueError("multiple variables found")
4331 if self
.variable
is None:
4332 raise ValueError("no variable found")
4333 self
.xaxis
= graph
.axes
[self
.variable
]
4334 unhandledcolumns
= self
.style
.setdata(graph
, {self
.variable
: 0, self
.result
: 1}, selectindex
, selecttotal
, self
)
4335 unhandledcolumnkeys
= unhandledcolumns
.keys()
4336 if len(unhandledcolumnkeys
):
4337 raise ValueError("style couldn't handle column keys %s" % unhandledcolumnkeys
)
4339 def adjustaxes(self
, graph
, step
):
4341 - on step == 0 axes with fixed data should be adjusted
4342 - on step == 1 the current axes ranges might be used to
4343 calculate further data (e.g. y data for a function y=f(x)
4344 where the y range depends on the x range)
4345 - on step == 2 axes ranges not previously set should be
4346 updated by data accumulated by step 1"""
4348 min, max = graph
.axes
[self
.variable
].getrange()
4349 if self
.min is not None: min = self
.min
4350 if self
.max is not None: max = self
.max
4351 vmin
= self
.xaxis
.convert(min)
4352 vmax
= self
.xaxis
.convert(max)
4354 for i
in range(self
.nopoints
):
4355 x
= self
.xaxis
.invert(vmin
+ (vmax
-vmin
)*i
/ (self
.nopoints
-1.0))
4356 self
.points
.append([x
])
4357 self
.style
.adjustaxes([self
.variable
], self
)
4359 for point
in self
.points
:
4360 self
.context
[self
.variable
] = point
[0]
4362 point
.append(self
.mathtree
.Calc(**self
.context
))
4363 except (ArithmeticError, ValueError):
4366 self
.style
.adjustaxes([self
.result
], self
)
4368 def draw(self
, graph
):
4369 self
.style
.drawpoints(graph
, self
)
4372 class paramfunction
:
4374 defaultstyle
= line()
4376 def __init__(self
, varname
, min, max, expression
, title
=helper
.nodefault
, points
=100, parser
=mathtree
.parser(), context
={}):
4377 if title
is helper
.nodefault
:
4378 self
.title
= expression
4381 self
.varname
= varname
4384 self
.points
= points
4385 self
.expression
= {}
4387 varlist
, expressionlist
= expression
.split("=")
4388 if mathtree
.__useparser
__ == mathtree
.__newparser
__: # XXX: switch between mathtree-parsers
4389 keys
= varlist
.split(",")
4390 mtrees
= helper
.ensurelist(parser
.parse(expressionlist
))
4391 if len(keys
) != len(mtrees
):
4392 raise ValueError("unpack tuple of wrong size")
4393 for i
in range(len(keys
)):
4394 key
= keys
[i
].strip()
4395 if self
.mathtrees
.has_key(key
):
4396 raise ValueError("multiple assignment in tuple")
4397 self
.mathtrees
[key
] = mtrees
[i
]
4398 if len(keys
) != len(self
.mathtrees
.keys()):
4399 raise ValueError("unpack tuple of wrong size")
4401 parsestr
= mathtree
.ParseStr(expressionlist
)
4402 for key
in varlist
.split(","):
4404 if self
.mathtrees
.has_key(key
):
4405 raise ValueError("multiple assignment in tuple")
4407 self
.mathtrees
[key
] = parser
.ParseMathTree(parsestr
)
4409 except mathtree
.CommaFoundMathTreeParseError
, e
:
4410 self
.mathtrees
[key
] = e
.MathTree
4412 raise ValueError("unpack tuple of wrong size")
4413 if len(varlist
.split(",")) != len(self
.mathtrees
.keys()):
4414 raise ValueError("unpack tuple of wrong size")
4416 for i
in range(self
.points
):
4417 context
[self
.varname
] = self
.min + (self
.max-self
.min)*i
/ (self
.points
-1.0)
4419 for key
, tree
in self
.mathtrees
.items():
4420 line
.append(tree
.Calc(**context
))
4421 self
.data
.append(line
)
4423 def setstyle(self
, graph
, style
):
4426 for key
, index
in zip(self
.mathtrees
.keys(), xrange(sys
.maxint
)):
4427 columns
[key
] = index
4428 self
.style
.setcolumns(graph
, columns
)
4430 def getranges(self
):
4431 return self
.style
.getranges(self
.data
)
4433 def setranges(self
, ranges
):
4436 def draw(self
, graph
):
4437 self
.style
.drawpoints(graph
, self
.data
)