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
)
63 class NoValidPartitionError(RuntimeError):
68 class _regularaxis(_axis
):
69 """base implementation a regular axis
71 Regular axis have a continuous variable like linear axes,
72 logarithmic axes, time axes etc."""
74 def __init__(self
, min=None, max=None, reverse
=0, divisor
=None, title
=None,
75 painter
=painter
.regular(), texter
=texter
.mixed(), linkpainter
=painter
.linked(),
76 density
=1, maxworse
=2, manualticks
=[]):
77 if min is not None and max is not None and min > max:
78 min, max, reverse
= max, min, not reverse
81 self
.reverse
= reverse
82 self
.divisor
= divisor
84 self
.painter
= painter
86 self
.linkpainter
= linkpainter
87 self
.density
= density
88 self
.maxworse
= maxworse
89 self
.manualticks
= self
.checkfraclist(manualticks
)
91 def createdata(self
, errorname
):
92 return axisdata(min=self
.min, max=self
.max)
96 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
97 if self
.min is None or self
.max is None:
98 for value
in columndata
:
100 value
= value
+ self
.zero
104 if self
.min is None and (data
.min is None or value
< data
.min):
106 if self
.max is None and (data
.max is None or value
> data
.max):
109 def checkfraclist(self
, fracs
):
110 "orders a list of fracs, equal entries are not allowed"
111 if not len(fracs
): return []
115 for item
in sorted[1:]:
117 raise ValueError("duplicate entry found")
121 def _create(self
, data
, positioner
, graphtexrunner
, parter
, rater
, errorname
):
122 errorname
= " for axis %s" % errorname
123 if data
.min is None or data
.max is None:
124 raise RuntimeError("incomplete axis range%s" % errorname
)
126 # a variant is a data copy with local modifications to test several partitions
128 def __init__(self
, data
, **kwargs
):
130 for key
, value
in kwargs
.items():
131 setattr(self
, key
, value
)
133 def __getattr__(self
, key
):
134 return getattr(data
, key
)
136 def __cmp__(self
, other
):
137 return cmp(self
.rate
, other
.rate
)
139 # build a list of variants
141 if parter
is not None:
142 if self
.divisor
is not None:
143 partfunctions
= parter
.partfunctions(data
.min/self
.divisor
, data
.max/self
.divisor
,
144 self
.min is None, self
.max is None)
146 partfunctions
= parter
.partfunctions(data
.min, data
.max,
147 self
.min is None, self
.max is None)
149 for partfunction
in partfunctions
:
151 while worse
< self
.maxworse
:
153 ticks
= partfunction()
156 ticks
= tick
.mergeticklists(self
.manualticks
, ticks
, mergeequal
=0)
158 rate
= rater
.rateticks(self
, ticks
, self
.density
)
160 rater
.raterange(self
.convert(data
, ticks
[0]) -
161 self
.convert(data
, ticks
[-1]), 1)
163 rater
.raterange(self
.convert(data
, ticks
[-1]) -
164 self
.convert(data
, ticks
[0]), 1)
165 if bestrate
is None or rate
< bestrate
:
168 variants
.append(variant(data
, rate
=rate
, ticks
=ticks
))
170 raise RuntimeError("no axis partitioning found%s" % errorname
)
172 variants
= [variant(data
, rate
=0, ticks
=self
.manualticks
)]
175 if self
.painter
is None or len(variants
) == 1:
176 # in case of a single variant we're almost done
177 self
.adjustaxis(data
, variants
[0].ticks
, graphtexrunner
, errorname
)
178 if variants
[0].ticks
:
179 self
.texter
.labels(variants
[0].ticks
)
181 for t
in variants
[0].ticks
:
182 t
*= tick
.rational(self
.divisor
)
183 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
184 data
.ticks
= variants
[0].ticks
185 if self
.painter
is not None:
186 self
.painter
.paint(canvas
, data
, self
, positioner
)
189 # build the layout for best variants
190 for variant
in variants
:
191 variant
.storedcanvas
= None
193 while not variants
[0].storedcanvas
:
194 self
.adjustaxis(variants
[0], variants
[0].ticks
, graphtexrunner
, errorname
)
195 if variants
[0].ticks
:
196 self
.texter
.labels(variants
[0].ticks
)
198 for t
in variants
[0].ticks
:
199 t
*= tick
.rational(self
.divisor
)
200 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
201 self
.painter
.paint(canvas
, variants
[0], self
, positioner
)
202 ratelayout
= rater
.ratelayout(canvas
, self
.density
)
203 if ratelayout
is None:
206 raise NoValidPartitionError("no valid axis partitioning found%s" % errorname
)
208 variants
[0].rate
+= ratelayout
209 variants
[0].storedcanvas
= canvas
211 self
.adjustaxis(data
, variants
[0].ticks
, graphtexrunner
, errorname
)
212 data
.ticks
= variants
[0].ticks
213 return variants
[0].storedcanvas
216 class linear(_regularaxis
):
219 def __init__(self
, parter
=parter
.autolinear(), rater
=rater
.linear(), **args
):
220 _regularaxis
.__init
__(self
, **args
)
224 def convert(self
, data
, value
):
225 """axis coordinates -> graph coordinates"""
227 return (data
.max - float(value
)) / (data
.max - data
.min)
229 return (float(value
) - data
.min) / (data
.max - data
.min)
231 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
232 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.parter
, self
.rater
, errorname
)
237 class logarithmic(_regularaxis
):
238 """logarithmic axis"""
240 def __init__(self
, parter
=parter
.autologarithmic(), rater
=rater
.logarithmic(),
241 linearparter
=parter
.autolinear(extendtick
=None), **args
):
242 _regularaxis
.__init
__(self
, **args
)
245 self
.linearparter
= linearparter
247 def convert(self
, data
, value
):
248 """axis coordinates -> graph coordinates"""
249 # TODO: store log(data.min) and log(data.max)
251 return (math
.log(data
.max) - math
.log(float(value
))) / (math
.log(data
.max) - math
.log(data
.min))
253 return (math
.log(float(value
)) - math
.log(data
.min)) / (math
.log(data
.max) - math
.log(data
.min))
255 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
257 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.parter
, self
.rater
, errorname
)
258 except NoValidPartitionError
:
259 if self
.linearparter
:
260 warnings
.warn("no valid logarithmic partitioning found for axis %s, switch to linear partitioning" % errorname
)
261 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.linearparter
, self
.rater
, errorname
)
267 class subaxispositioner(positioner
._positioner
):
268 """a subaxis positioner"""
270 def __init__(self
, basepositioner
, subaxis
):
271 self
.basepositioner
= basepositioner
272 self
.vmin
= subaxis
.vmin
273 self
.vmax
= subaxis
.vmax
274 self
.vminover
= subaxis
.vminover
275 self
.vmaxover
= subaxis
.vmaxover
277 def vbasepath(self
, v1
=None, v2
=None):
279 v1
= self
.vmin
+v1
*(self
.vmax
-self
.vmin
)
283 v2
= self
.vmin
+v2
*(self
.vmax
-self
.vmin
)
286 return self
.basepositioner
.vbasepath(v1
, v2
)
288 def vgridpath(self
, v
):
289 return self
.basepositioner
.vgridpath(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
291 def vtickpoint_pt(self
, v
, axis
=None):
292 return self
.basepositioner
.vtickpoint_pt(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
294 def vtickdirection(self
, v
, axis
=None):
295 return self
.basepositioner
.vtickdirection(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
300 def __init__(self
, subaxes
=None, defaultsubaxis
=linear(painter
=None, linkpainter
=None, parter
=None, texter
=None),
301 dist
=0.5, firstdist
=None, lastdist
=None, title
=None, reverse
=0,
302 painter
=painter
.bar(), linkpainter
=painter
.linkedbar()):
303 self
.subaxes
= subaxes
304 self
.defaultsubaxis
= defaultsubaxis
306 if firstdist
is not None:
307 self
.firstdist
= firstdist
309 self
.firstdist
= 0.5 * dist
310 if lastdist
is not None:
311 self
.lastdist
= lastdist
313 self
.lastdist
= 0.5 * dist
315 self
.reverse
= reverse
316 self
.painter
= painter
317 self
.linkpainter
= linkpainter
319 def createdata(self
, errorname
):
320 data
= axisdata(size
=self
.firstdist
+self
.lastdist
-self
.dist
, subaxes
={}, names
=[])
323 def addsubaxis(self
, data
, name
, subaxis
, graphtexrunner
, errorname
):
324 subaxis
= anchoredaxis(subaxis
, graphtexrunner
, "%s, subaxis %s" % (errorname
, name
))
325 subaxis
.setcreatecall(lambda: None)
326 subaxis
.sized
= hasattr(subaxis
.data
, "size")
328 data
.size
+= subaxis
.data
.size
331 data
.size
+= self
.dist
332 data
.subaxes
[name
] = subaxis
334 data
.names
.insert(0, name
)
336 data
.names
.append(name
)
338 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
339 for value
in columndata
:
341 # some checks and error messages
345 raise ValueError("tuple expected by bar axis '%s'" % errorname
)
351 raise ValueError("tuple expected by bar axis '%s'" % errorname
)
352 assert len(value
) == 2, "tuple of size two expected by bar axis '%s'" % errorname
355 if name
is not None and name
not in data
.names
:
357 if self
.subaxes
[name
] is not None:
358 self
.addsubaxis(data
, name
, self
.subaxes
[name
], graphtexrunner
, errorname
)
360 self
.addsubaxis(data
, name
, self
.defaultsubaxis
, graphtexrunner
, errorname
)
361 for name
in data
.names
:
362 subaxis
= data
.subaxes
[name
]
364 data
.size
-= subaxis
.data
.size
365 subaxis
.axis
.adjustaxis(subaxis
.data
,
366 [value
[1] for value
in columndata
if value
[0] == name
],
368 "%s, subaxis %s" % (errorname
, name
))
370 data
.size
+= subaxis
.data
.size
372 def convert(self
, data
, value
):
375 axis
= data
.subaxes
[value
[0]]
378 return axis
.vmin
+ axis
.convert(value
[1]) * (axis
.vmax
- axis
.vmin
)
380 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
381 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
383 position
= self
.firstdist
384 for name
in data
.names
:
385 subaxis
= data
.subaxes
[name
]
386 subaxis
.vmin
= position
/ float(data
.size
)
388 position
+= subaxis
.data
.size
391 subaxis
.vmax
= position
/ float(data
.size
)
392 position
+= 0.5*self
.dist
394 if name
== data
.names
[-1]:
397 subaxis
.vmaxover
= position
/ float(data
.size
)
398 subaxis
.setpositioner(subaxispositioner(positioner
, subaxis
))
400 canvas
.insert(subaxis
.canvas
)
401 if canvas
.extent_pt
< subaxis
.canvas
.extent_pt
:
402 canvas
.extent_pt
= subaxis
.canvas
.extent_pt
403 position
+= 0.5*self
.dist
405 if self
.painter
is not None:
406 self
.painter
.paint(canvas
, data
, self
, positioner
)
409 def createlinked(self
, data
, positioner
, graphtexrunner
, errorname
, linkpainter
):
410 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
411 for name
in data
.names
:
412 subaxis
= data
.subaxes
[name
]
413 subaxis
= linkedaxis(subaxis
, name
)
414 subaxis
.setpositioner(subaxispositioner(positioner
, data
.subaxes
[name
]))
416 canvas
.insert(subaxis
.canvas
)
417 if canvas
.extent_pt
< subaxis
.canvas
.extent_pt
:
418 canvas
.extent_pt
= subaxis
.canvas
.extent_pt
419 if linkpainter
is not None:
420 linkpainter
.paint(canvas
, data
, self
, positioner
)
424 class nestedbar(bar
):
426 def __init__(self
, defaultsubaxis
=bar(dist
=0, painter
=None, linkpainter
=None), **kwargs
):
427 bar
.__init
__(self
, defaultsubaxis
=defaultsubaxis
, **kwargs
)
432 def __init__(self
, defaultsubaxis
=linear(),
433 firstdist
=0, lastdist
=0,
434 painter
=painter
.split(), linkpainter
=painter
.linkedsplit(), **kwargs
):
435 bar
.__init
__(self
, defaultsubaxis
=defaultsubaxis
,
436 firstdist
=firstdist
, lastdist
=lastdist
,
437 painter
=painter
, linkpainter
=linkpainter
, **kwargs
)
440 class sizedlinear(linear
):
442 def __init__(self
, size
=1, **kwargs
):
443 linear
.__init
__(self
, **kwargs
)
446 def createdata(self
, errorname
):
447 data
= linear
.createdata(self
, errorname
)
448 data
.size
= self
.size
451 sizedlin
= sizedlinear
454 class autosizedlinear(linear
):
456 def __init__(self
, parter
=parter
.autolinear(extendtick
=None), **kwargs
):
457 linear
.__init
__(self
, parter
=parter
, **kwargs
)
459 def createdata(self
, errorname
):
460 data
= linear
.createdata(self
, errorname
)
462 data
.size
= data
.max - data
.min
467 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
468 linear
.adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
)
470 data
.size
= data
.max - data
.min
474 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
477 canvas
= linear
.create(self
, data
, positioner
, graphtexrunner
, errorname
)
478 if min != data
.min or max != data
.max:
479 raise RuntimeError("range change during axis creation of autosized linear axis")
482 autosizedlin
= autosizedlinear
487 def __init__(self
, axis
, graphtexrunner
, errorname
):
488 assert not isinstance(axis
, anchoredaxis
), errorname
490 self
.errorname
= errorname
491 self
.graphtexrunner
= graphtexrunner
492 self
.data
= axis
.createdata(errorname
)
494 self
.positioner
= None
496 def setcreatecall(self
, function
, *args
, **kwargs
):
497 self
._createfunction
= function
498 self
._createargs
= args
499 self
._createkwargs
= kwargs
503 self
._createfunction
(*self
._createargs
, **self
._createkwargs
)
505 def setpositioner(self
, positioner
):
506 assert positioner
is not None, self
.errorname
507 assert self
.positioner
is None, self
.errorname
508 self
.positioner
= positioner
510 def convert(self
, x
):
512 return self
.axis
.convert(self
.data
, x
)
514 def adjustaxis(self
, columndata
):
515 if self
.canvas
is None:
516 self
.axis
.adjustaxis(self
.data
, columndata
, self
.graphtexrunner
, self
.errorname
)
518 warnings
.warn("ignore axis range adjustment of already created axis '%s'" % self
.errorname
)
520 def vbasepath(self
, v1
=None, v2
=None):
521 return self
.positioner
.vbasepath(v1
=v1
, v2
=v2
)
523 def basepath(self
, x1
=None, x2
=None):
527 return self
.positioner
.vbasepath()
529 return self
.positioner
.vbasepath(v2
=self
.axis
.convert(self
.data
, x2
))
532 return self
.positioner
.vbasepath(v1
=self
.axis
.convert(self
.data
, x1
))
534 return self
.positioner
.vbasepath(v1
=self
.axis
.convert(self
.data
, x1
),
535 v2
=self
.axis
.convert(self
.data
, x2
))
537 def vgridpath(self
, v
):
538 return self
.positioner
.vgridpath(v
)
540 def gridpath(self
, x
):
542 return self
.positioner
.vgridpath(self
.axis
.convert(self
.data
, x
))
544 def vtickpoint_pt(self
, v
):
545 return self
.positioner
.vtickpoint_pt(v
)
547 def vtickpoint(self
, v
):
548 return self
.positioner
.vtickpoint_pt(v
) * unit
.t_pt
550 def tickpoint_pt(self
, x
):
552 return self
.positioner
.vtickpoint_pt(self
.axis
.convert(self
.data
, x
))
554 def tickpoint(self
, x
):
556 x_pt
, y_pt
= self
.positioner
.vtickpoint_pt(self
.axis
.convert(self
.data
, x
))
557 return x_pt
* unit
.t_pt
, y_pt
* unit
.t_pt
559 def vtickdirection(self
, v
):
560 return self
.positioner
.vtickdirection(v
)
562 def tickdirection(self
, x
):
564 return self
.positioner
.vtickdirection(self
.axis
.convert(self
.data
, x
))
567 if self
.canvas
is None:
568 assert self
.positioner
is not None, self
.errorname
569 self
.canvas
= self
.axis
.create(self
.data
, self
.positioner
, self
.graphtexrunner
, self
.errorname
)
573 class linkedaxis(anchoredaxis
):
575 def __init__(self
, linkedaxis
=None, errorname
="manual-linked", painter
=_marker
):
576 self
.painter
= painter
578 self
.errorname
= errorname
580 self
.positioner
= None
582 self
.setlinkedaxis(linkedaxis
)
584 def setlinkedaxis(self
, linkedaxis
):
585 assert isinstance(linkedaxis
, anchoredaxis
), errorname
586 self
.linkedto
= linkedaxis
587 self
.axis
= linkedaxis
.axis
588 self
.graphtexrunner
= self
.linkedto
.graphtexrunner
589 self
.errorname
= "%s (linked to %s)" % (self
.errorname
, linkedaxis
.errorname
)
590 self
.data
= linkedaxis
.data
591 if self
.painter
is _marker
:
592 self
.painter
= linkedaxis
.axis
.linkpainter
595 assert self
.linkedto
is not None, self
.errorname
596 assert self
.positioner
is not None, self
.errorname
597 if self
.canvas
is None:
598 self
.linkedto
.docreate()
599 self
.canvas
= self
.axis
.createlinked(self
.data
, self
.positioner
, self
.graphtexrunner
, self
.errorname
, self
.painter
)
603 class anchoredpathaxis(anchoredaxis
):
604 """an anchored axis along a path"""
606 def __init__(self
, path
, axis
, **kwargs
):
607 anchoredaxis
.__init
__(self
, axis
, text
.defaulttexrunner
, "pathaxis")
608 self
.setpositioner(positioner
.pathpositioner(path
, **kwargs
))
611 def pathaxis(*args
, **kwargs
):
612 """creates an axiscanvas for an axis along a path"""
613 return anchoredpathaxis(*args
, **kwargs
).canvas