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-2005 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 from __future__
import nested_scopes
28 from pyx
import attr
, unit
, text
29 from pyx
.graph
.axis
import painter
, parter
, positioner
, rater
, texter
, tick
34 # fallback implementation for Python 2.2 and below
36 return zip(xrange(len(list)), list)
42 """axis data storage class
44 Instances of this class are used to store axis data local to the
45 graph. It will always contain an axispos instance provided by the
46 graph during initialization."""
48 def __init__(self
, **kwargs
):
49 for key
, value
in kwargs
.items():
50 setattr(self
, key
, value
)
56 def createlinked(self
, data
, positioner
, graphtexrunner
, errorname
, linkpainter
):
57 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
58 if linkpainter
is not None:
59 linkpainter
.paint(canvas
, data
, self
, positioner
)
64 "linear conversion methods"
66 def convert(self
, data
, value
):
67 """axis coordinates -> graph coordinates"""
69 return (data
.max - float(value
)) / (data
.max - data
.min)
71 return (float(value
) - data
.min) / (data
.max - data
.min)
75 "logarithmic convertion methods"
77 def convert(self
, data
, value
):
78 """axis coordinates -> graph coordinates"""
79 # TODO: store log(data.min) and log(data.max)
81 return (math
.log(data
.max) - math
.log(float(value
))) / (math
.log(data
.max) - math
.log(data
.min))
83 return (math
.log(float(value
)) - math
.log(data
.min)) / (math
.log(data
.max) - math
.log(data
.min))
86 class _regularaxis(_axis
):
87 """base implementation a regular axis
89 Regular axis have a continuous variable like linear axes,
90 logarithmic axes, time axes etc."""
92 def __init__(self
, min=None, max=None, reverse
=0, divisor
=None, title
=None,
93 painter
=painter
.regular(), texter
=texter
.mixed(), linkpainter
=painter
.linked(),
94 density
=1, maxworse
=2, manualticks
=[]):
95 if min is not None and max is not None and min > max:
96 min, max, reverse
= max, min, not reverse
99 self
.reverse
= reverse
100 self
.divisor
= divisor
102 self
.painter
= painter
104 self
.linkpainter
= linkpainter
105 self
.density
= density
106 self
.maxworse
= maxworse
107 self
.manualticks
= self
.checkfraclist(manualticks
)
109 def createdata(self
, errorname
):
110 return axisdata(min=self
.min, max=self
.max)
114 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
115 if self
.min is None or self
.max is None:
116 for value
in columndata
:
118 value
= value
+ self
.zero
122 if self
.min is None and (data
.min is None or value
< data
.min):
124 if self
.max is None and (data
.max is None or value
> data
.max):
127 def checkfraclist(self
, fracs
):
128 "orders a list of fracs, equal entries are not allowed"
129 if not len(fracs
): return []
133 for item
in sorted[1:]:
135 raise ValueError("duplicate entry found")
139 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
140 errorname
= " for axis %s" % errorname
141 if data
.min is None or data
.max is None:
142 raise RuntimeError("incomplete axis range%s" % errorname
)
144 # a variant is a data copy with local modifications to test several partitions
146 def __init__(self
, data
, **kwargs
):
148 for key
, value
in kwargs
.items():
149 setattr(self
, key
, value
)
151 def __getattr__(self
, key
):
152 return getattr(data
, key
)
154 def __cmp__(self
, other
):
155 return cmp(self
.rate
, other
.rate
)
157 # build a list of variants
159 if self
.parter
is not None:
160 if self
.divisor
is not None:
161 partfunctions
= self
.parter
.partfunctions(data
.min/self
.divisor
, data
.max/self
.divisor
,
162 self
.min is None, self
.max is None)
164 partfunctions
= self
.parter
.partfunctions(data
.min, data
.max,
165 self
.min is None, self
.max is None)
167 for partfunction
in partfunctions
:
169 while worse
< self
.maxworse
:
171 ticks
= partfunction()
174 ticks
= tick
.mergeticklists(self
.manualticks
, ticks
, mergeequal
=0)
176 rate
= ( self
.rater
.rateticks(self
, ticks
, self
.density
) +
177 self
.rater
.raterange(self
.convert(data
, ticks
[-1]) -
178 self
.convert(data
, ticks
[0]), 1) )
179 if bestrate
is None or rate
< bestrate
:
182 variants
.append(variant(data
, rate
=rate
, ticks
=ticks
))
184 raise RuntimeError("no axis partitioning found%s" % errorname
)
186 variants
= [variant(data
, rate
=0, ticks
=self
.manualticks
)]
189 if self
.painter
is None or len(variants
) == 1:
190 # in case of a single variant we're almost done
191 self
.adjustaxis(data
, variants
[0].ticks
, graphtexrunner
, errorname
)
192 if variants
[0].ticks
:
193 self
.texter
.labels(variants
[0].ticks
)
195 for t
in variants
[0].ticks
:
196 t
*= tick
.rational(self
.divisor
)
197 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
198 data
.ticks
= variants
[0].ticks
199 if self
.painter
is not None:
200 self
.painter
.paint(canvas
, data
, self
, positioner
)
203 # build the layout for best variants
204 for variant
in variants
:
205 variant
.storedcanvas
= None
207 while not variants
[0].storedcanvas
:
208 self
.adjustaxis(variants
[0], variants
[0].ticks
, graphtexrunner
, errorname
)
209 if variants
[0].ticks
:
210 self
.texter
.labels(variants
[0].ticks
)
212 for t
in variants
[0].ticks
:
213 t
*= tick
.rational(self
.divisor
)
214 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
215 self
.painter
.paint(canvas
, variants
[0], self
, positioner
)
216 ratelayout
= self
.rater
.ratelayout(canvas
, self
.density
)
217 if ratelayout
is None:
220 raise RuntimeError("no valid axis partitioning found%s" % errorname
)
222 variants
[0].rate
+= ratelayout
223 variants
[0].storedcanvas
= canvas
225 self
.adjustaxis(data
, variants
[0].ticks
, graphtexrunner
, errorname
)
226 data
.ticks
= variants
[0].ticks
227 return variants
[0].storedcanvas
230 class linear(_regularaxis
, _linmap
):
233 def __init__(self
, parter
=parter
.autolinear(), rater
=rater
.linear(), **args
):
234 _regularaxis
.__init
__(self
, **args
)
241 class logarithmic(_regularaxis
, _logmap
):
242 """logarithmic axis"""
244 def __init__(self
, parter
=parter
.autologarithmic(), rater
=rater
.logarithmic(), **args
):
245 _regularaxis
.__init
__(self
, **args
)
252 class subaxispositioner(positioner
._positioner
):
253 """a subaxis positioner"""
255 def __init__(self
, basepositioner
, subaxis
):
256 self
.basepositioner
= basepositioner
257 self
.vmin
= subaxis
.vmin
258 self
.vmax
= subaxis
.vmax
259 self
.vminover
= subaxis
.vminover
260 self
.vmaxover
= subaxis
.vmaxover
262 def vbasepath(self
, v1
=None, v2
=None):
264 v1
= self
.vmin
+v1
*(self
.vmax
-self
.vmin
)
268 v2
= self
.vmin
+v2
*(self
.vmax
-self
.vmin
)
271 return self
.basepositioner
.vbasepath(v1
, v2
)
273 def vgridpath(self
, v
):
274 return self
.basepositioner
.vgridpath(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
276 def vtickpoint_pt(self
, v
, axis
=None):
277 return self
.basepositioner
.vtickpoint_pt(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
279 def vtickdirection(self
, v
, axis
=None):
280 return self
.basepositioner
.vtickdirection(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
285 def __init__(self
, subaxes
=None, defaultsubaxis
=linear(painter
=None, linkpainter
=None, parter
=None, texter
=None),
286 dist
=0.5, firstdist
=None, lastdist
=None, title
=None, reverse
=0,
287 painter
=painter
.bar(), linkpainter
=painter
.linkedbar()):
288 self
.subaxes
= subaxes
289 self
.defaultsubaxis
= defaultsubaxis
291 if firstdist
is not None:
292 self
.firstdist
= firstdist
294 self
.firstdist
= 0.5 * dist
295 if lastdist
is not None:
296 self
.lastdist
= lastdist
298 self
.lastdist
= 0.5 * dist
300 self
.reverse
= reverse
301 self
.painter
= painter
302 self
.linkpainter
= linkpainter
304 def createdata(self
, errorname
):
305 data
= axisdata(size
=self
.firstdist
+self
.lastdist
-self
.dist
, subaxes
={}, names
=[])
308 def addsubaxis(self
, data
, name
, subaxis
, graphtexrunner
, errorname
):
309 subaxis
= anchoredaxis(subaxis
, graphtexrunner
, "%s, subaxis %s" % (errorname
, name
))
310 subaxis
.sized
= hasattr(subaxis
.data
, "size")
312 data
.size
+= subaxis
.data
.size
315 data
.size
+= self
.dist
316 data
.subaxes
[name
] = subaxis
318 data
.names
.insert(0, name
)
320 data
.names
.append(name
)
322 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
323 for value
in columndata
:
325 # some checks and error messages
329 raise ValueError("tuple expected by bar axis '%s'" % errorname
)
335 raise ValueError("tuple expected by bar axis '%s'" % errorname
)
336 assert len(value
) == 2, "tuple of size two expected by bar axis '%s'" % errorname
339 if name
is not None and name
not in data
.names
:
341 if self
.subaxes
[name
] is not None:
342 self
.addsubaxis(data
, name
, self
.subaxes
[name
], graphtexrunner
, errorname
)
344 self
.addsubaxis(data
, name
, self
.defaultsubaxis
, graphtexrunner
, errorname
)
345 for name
in data
.names
:
346 subaxis
= data
.subaxes
[name
]
348 data
.size
-= subaxis
.data
.size
349 subaxis
.axis
.adjustaxis(subaxis
.data
,
350 [value
[1] for value
in columndata
if value
[0] == name
],
352 "%s, subaxis %s" % (errorname
, name
))
354 data
.size
+= subaxis
.data
.size
356 def convert(self
, data
, value
):
359 axis
= data
.subaxes
[value
[0]]
362 return axis
.vmin
+ axis
.convert(value
[1]) * (axis
.vmax
- axis
.vmin
)
364 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
365 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
367 position
= self
.firstdist
368 for name
in data
.names
:
369 subaxis
= data
.subaxes
[name
]
370 subaxis
.vmin
= position
/ float(data
.size
)
372 position
+= subaxis
.data
.size
375 subaxis
.vmax
= position
/ float(data
.size
)
376 position
+= 0.5*self
.dist
378 if name
== data
.names
[-1]:
381 subaxis
.vmaxover
= position
/ float(data
.size
)
382 subaxis
.setpositioner(subaxispositioner(positioner
, subaxis
))
384 canvas
.insert(subaxis
.canvas
)
385 if canvas
.extent_pt
< subaxis
.canvas
.extent_pt
:
386 canvas
.extent_pt
= subaxis
.canvas
.extent_pt
387 position
+= 0.5*self
.dist
389 if self
.painter
is not None:
390 self
.painter
.paint(canvas
, data
, self
, positioner
)
393 def createlinked(self
, data
, positioner
, graphtexrunner
, errorname
, linkpainter
):
394 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
395 for name
in data
.names
:
396 subaxis
= data
.subaxes
[name
]
397 subaxis
= linkedaxis(subaxis
, name
)
398 subaxis
.setpositioner(subaxispositioner(positioner
, data
.subaxes
[name
]))
400 canvas
.insert(subaxis
.canvas
)
401 if canvas
.extent_pt
< subaxis
.canvas
.extent_pt
:
402 canvas
.extent_pt
= subaxis
.canvas
.extent_pt
403 if linkpainter
is not None:
404 linkpainter
.paint(canvas
, data
, self
, positioner
)
408 class nestedbar(bar
):
410 def __init__(self
, defaultsubaxis
=bar(dist
=0, painter
=None, linkpainter
=None), **kwargs
):
411 bar
.__init
__(self
, defaultsubaxis
=defaultsubaxis
, **kwargs
)
416 def __init__(self
, defaultsubaxis
=linear(),
417 firstdist
=0, lastdist
=0,
418 painter
=painter
.split(), linkpainter
=painter
.linkedsplit(), **kwargs
):
419 bar
.__init
__(self
, defaultsubaxis
=defaultsubaxis
,
420 firstdist
=firstdist
, lastdist
=lastdist
,
421 painter
=painter
, linkpainter
=linkpainter
, **kwargs
)
424 class sizedlinear(linear
):
426 def __init__(self
, size
=1, **kwargs
):
427 linear
.__init
__(self
, **kwargs
)
430 def createdata(self
, errorname
):
431 data
= linear
.createdata(self
, errorname
)
432 data
.size
= self
.size
435 sizedlin
= sizedlinear
438 class autosizedlinear(linear
):
440 def __init__(self
, parter
=parter
.autolinear(extendtick
=None), **kwargs
):
441 linear
.__init
__(self
, parter
=parter
, **kwargs
)
443 def createdata(self
, errorname
):
444 data
= linear
.createdata(self
, errorname
)
446 data
.size
= data
.max - data
.min
451 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
452 linear
.adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
)
454 data
.size
= data
.max - data
.min
458 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
461 canvas
= linear
.create(self
, data
, positioner
, graphtexrunner
, errorname
)
462 if min != data
.min or max != data
.max:
463 raise RuntimeError("range change during axis creation of autosized linear axis")
466 autosizedlin
= autosizedlinear
471 def __init__(self
, axis
, graphtexrunner
, errorname
):
472 assert not isinstance(axis
, anchoredaxis
), errorname
474 self
.errorname
= errorname
475 self
.graphtexrunner
= graphtexrunner
476 self
.data
= axis
.createdata(errorname
)
478 self
.positioner
= None
480 def setcreatecall(self
, function
, *args
, **kwargs
):
481 self
._createfunction
= function
482 self
._createargs
= args
483 self
._createkwargs
= kwargs
487 self
._createfunction
(*self
._createargs
, **self
._createkwargs
)
489 def setpositioner(self
, positioner
):
490 assert positioner
is not None, self
.errorname
491 assert self
.positioner
is None, self
.errorname
492 self
.positioner
= positioner
494 def convert(self
, x
):
496 return self
.axis
.convert(self
.data
, x
)
498 def adjustaxis(self
, columndata
):
499 if self
.canvas
is None:
500 self
.axis
.adjustaxis(self
.data
, columndata
, self
.graphtexrunner
, self
.errorname
)
502 warnings
.warn("ignore axis range adjustment of already created axis '%s'" % self
.errorname
)
504 def vbasepath(self
, v1
=None, v2
=None):
505 return self
.positioner
.vbasepath(v1
=v1
, v2
=v2
)
507 def basepath(self
, x1
=None, x2
=None):
511 return self
.positioner
.vbasepath()
513 return self
.positioner
.vbasepath(v2
=self
.axis
.convert(self
.data
, x2
))
516 return self
.positioner
.vbasepath(v1
=self
.axis
.convert(self
.data
, x1
))
518 return self
.positioner
.vbasepath(v1
=self
.axis
.convert(self
.data
, x1
),
519 v2
=self
.axis
.convert(self
.data
, x2
))
521 def vgridpath(self
, v
):
522 return self
.positioner
.vgridpath(v
)
524 def gridpath(self
, x
):
526 return self
.positioner
.vgridpath(self
.axis
.convert(self
.data
, x
))
528 def vtickpoint_pt(self
, v
):
529 return self
.positioner
.vtickpoint_pt(v
)
531 def vtickpoint(self
, v
):
532 return self
.positioner
.vtickpoint_pt(v
) * unit
.t_pt
534 def tickpoint_pt(self
, x
):
536 return self
.positioner
.vtickpoint_pt(self
.axis
.convert(self
.data
, x
))
538 def tickpoint(self
, x
):
540 x_pt
, y_pt
= self
.positioner
.vtickpoint_pt(self
.axis
.convert(self
.data
, x
))
541 return x_pt
* unit
.t_pt
, y_pt
* unit
.t_pt
543 def vtickdirection(self
, v
):
544 return self
.positioner
.vtickdirection(v
)
546 def tickdirection(self
, x
):
548 return self
.positioner
.vtickdirection(self
.axis
.convert(self
.data
, x
))
551 if self
.canvas
is None:
552 assert self
.positioner
is not None, self
.errorname
553 self
.canvas
= self
.axis
.create(self
.data
, self
.positioner
, self
.graphtexrunner
, self
.errorname
)
557 class linkedaxis(anchoredaxis
):
559 def __init__(self
, linkedaxis
=None, errorname
="manual-linked", painter
=_marker
):
560 self
.painter
= painter
562 self
.errorname
= errorname
564 self
.positioner
= None
566 self
.setlinkedaxis(linkedaxis
)
568 def setlinkedaxis(self
, linkedaxis
):
569 assert isinstance(linkedaxis
, anchoredaxis
), errorname
570 self
.linkedto
= linkedaxis
571 self
.axis
= linkedaxis
.axis
572 self
.graphtexrunner
= self
.linkedto
.graphtexrunner
573 self
.errorname
= "%s (linked to %s)" % (self
.errorname
, linkedaxis
.errorname
)
574 self
.data
= linkedaxis
.data
575 if self
.painter
is _marker
:
576 self
.painter
= linkedaxis
.axis
.linkpainter
579 assert self
.linkedto
is not None, self
.errorname
580 assert self
.positioner
is not None, self
.errorname
581 if self
.canvas
is None:
582 self
.linkedto
.docreate()
583 self
.canvas
= self
.axis
.createlinked(self
.data
, self
.positioner
, self
.graphtexrunner
, self
.errorname
, self
.painter
)
587 class anchoredpathaxis(anchoredaxis
):
588 """an anchored axis along a path"""
590 def __init__(self
, path
, axis
, **kwargs
):
591 anchoredaxis
.__init
__(self
, axis
, text
.defaulttexrunner
, "pathaxis")
592 self
.setpositioner(positioner
.pathpositioner(path
, **kwargs
))
595 def pathaxis(*args
, **kwargs
):
596 """creates an axiscanvas for an axis along a path"""
597 return anchoredpathaxis(*args
, **kwargs
).canvas