1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2011 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2011 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 from pyx
import attr
, unit
, text
26 from pyx
.graph
.axis
import painter
, parter
, positioner
, rater
, texter
, tick
33 """axis data storage class
35 Instances of this class are used to store axis data local to the
36 graph. It will always contain an axispos instance provided by the
37 graph during initialization."""
39 def __init__(self
, **kwargs
):
40 for key
, value
in kwargs
.items():
41 setattr(self
, key
, value
)
47 def createlinked(self
, data
, positioner
, graphtexrunner
, errorname
, linkpainter
):
48 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
49 if linkpainter
is not None:
50 linkpainter
.paint(canvas
, data
, self
, positioner
)
54 class NoValidPartitionError(RuntimeError):
59 class _regularaxis(_axis
):
60 """base implementation a regular axis
62 Regular axis have a continuous variable like linear axes,
63 logarithmic axes, time axes etc."""
65 def __init__(self
, min=None, max=None, reverse
=0, divisor
=None, title
=None,
66 painter
=painter
.regular(), texter
=texter
.mixed(), linkpainter
=painter
.linked(),
67 density
=1, maxworse
=2, manualticks
=[], fallbackrange
=None):
68 if min is not None and max is not None and min > max:
69 min, max, reverse
= max, min, not reverse
72 self
.reverse
= reverse
73 self
.divisor
= divisor
75 self
.painter
= painter
77 self
.linkpainter
= linkpainter
78 self
.density
= density
79 self
.maxworse
= maxworse
80 self
.manualticks
= self
.checkfraclist(manualticks
)
81 self
.fallbackrange
= fallbackrange
83 def createdata(self
, errorname
):
84 return axisdata(min=self
.min, max=self
.max)
88 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
89 if self
.min is None or self
.max is None:
90 for value
in columndata
:
92 value
= value
+ self
.zero
96 if self
.min is None and (data
.min is None or value
< data
.min):
98 if self
.max is None and (data
.max is None or value
> data
.max):
101 def checkfraclist(self
, fracs
):
102 "orders a list of fracs, equal entries are not allowed"
103 if not len(fracs
): return []
107 for item
in sorted[1:]:
109 raise ValueError("duplicate entry found")
113 def _create(self
, data
, positioner
, graphtexrunner
, parter
, rater
, errorname
):
114 errorname
= " for axis %s" % errorname
115 if data
.min is None or data
.max is None:
116 raise RuntimeError("incomplete axis range%s" % errorname
)
117 if data
.max == data
.min:
118 if self
.fallbackrange
is not None:
120 data
.min, data
.max = data
.min - 0.5*self
.fallbackrange
, data
.min + 0.5*self
.fallbackrange
122 data
.min, data
.max = self
.fallbackrange
[0], self
.fallbackrange
[1]
124 raise RuntimeError("zero axis range%s" % errorname
)
127 self
.adjustaxis(data
, data
.ticks
, graphtexrunner
, errorname
)
128 self
.texter
.labels(data
.ticks
)
131 t
*= tick
.rational(self
.divisor
)
132 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
133 if self
.painter
is not None:
134 self
.painter
.paint(canvas
, data
, self
, positioner
)
138 data
.ticks
= self
.manualticks
141 # a variant is a data copy with local modifications to test several partitions
143 def __init__(self
, data
, **kwargs
):
145 for key
, value
in kwargs
.items():
146 setattr(self
, key
, value
)
148 def __getattr__(self
, key
):
149 return getattr(data
, key
)
151 def __cmp__(self
, other
):
152 # we can also sort variants by their rate
153 return cmp(self
.rate
, other
.rate
)
155 # build a list of variants
157 if self
.divisor
is not None:
158 if data
.min is not None:
159 data_min_divided
= data
.min/self
.divisor
161 data_min_divided
= None
162 if data
.max is not None:
163 data_max_divided
= data
.max/self
.divisor
165 data_max_divided
= None
166 partfunctions
= parter
.partfunctions(data_min_divided
, data_max_divided
,
167 self
.min is None, self
.max is None)
169 partfunctions
= parter
.partfunctions(data
.min, data
.max,
170 self
.min is None, self
.max is None)
172 for partfunction
in partfunctions
:
174 while worse
< self
.maxworse
:
176 ticks
= partfunction()
179 ticks
= [t
for t
in tick
.mergeticklists(self
.manualticks
, ticks
, mergeequal
=0)
180 if t
.ticklevel
is not None or t
.labellevel
is not None]
182 rate
= rater
.rateticks(self
, ticks
, self
.density
)
184 rate
+= rater
.raterange(self
.convert(data
, ticks
[0]) -
185 self
.convert(data
, ticks
[-1]), 1)
187 rate
+= rater
.raterange(self
.convert(data
, ticks
[-1]) -
188 self
.convert(data
, ticks
[0]), 1)
189 if bestrate
is None or rate
< bestrate
:
192 variants
.append(variant(data
, rate
=rate
, ticks
=ticks
))
195 raise RuntimeError("no axis partitioning found%s" % errorname
)
197 if len(variants
) == 1 or self
.painter
is None:
198 # When the painter is None, we could sort the variants here by their rating.
199 # However, we didn't did this so far and there is no real reason to change that.
200 data
.ticks
= variants
[0].ticks
203 # build the layout for best variants
204 for variant
in variants
:
205 variant
.storedcanvas
= None
207 while not variants
[0].storedcanvas
:
208 variants
[0].storedcanvas
= layout(variants
[0])
209 ratelayout
= rater
.ratelayout(variants
[0].storedcanvas
, self
.density
)
210 if ratelayout
is None:
213 raise NoValidPartitionError("no valid axis partitioning found%s" % errorname
)
215 variants
[0].rate
+= ratelayout
217 self
.adjustaxis(data
, variants
[0].ticks
, graphtexrunner
, errorname
)
218 data
.ticks
= variants
[0].ticks
219 return variants
[0].storedcanvas
222 class linear(_regularaxis
):
225 def __init__(self
, parter
=parter
.autolinear(), rater
=rater
.linear(), **args
):
226 _regularaxis
.__init
__(self
, **args
)
230 def convert(self
, data
, value
):
231 """axis coordinates -> graph coordinates"""
233 return (data
.max - float(value
)) / (data
.max - data
.min)
235 return (float(value
) - data
.min) / (data
.max - data
.min)
237 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
238 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.parter
, self
.rater
, errorname
)
243 class logarithmic(_regularaxis
):
244 """logarithmic axis"""
246 def __init__(self
, parter
=parter
.autologarithmic(), rater
=rater
.logarithmic(),
247 linearparter
=parter
.autolinear(extendtick
=None), **args
):
248 _regularaxis
.__init
__(self
, **args
)
251 self
.linearparter
= linearparter
253 def convert(self
, data
, value
):
254 """axis coordinates -> graph coordinates"""
255 # TODO: store log(data.min) and log(data.max)
257 return (math
.log(data
.max) - math
.log(float(value
))) / (math
.log(data
.max) - math
.log(data
.min))
259 return (math
.log(float(value
)) - math
.log(data
.min)) / (math
.log(data
.max) - math
.log(data
.min))
261 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
263 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.parter
, self
.rater
, errorname
)
264 except NoValidPartitionError
:
265 if self
.linearparter
:
266 warnings
.warn("no valid logarithmic partitioning found for axis %s, switch to linear partitioning" % errorname
)
267 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.linearparter
, self
.rater
, errorname
)
273 class subaxispositioner(positioner
._positioner
):
274 """a subaxis positioner"""
276 def __init__(self
, basepositioner
, subaxis
):
277 self
.basepositioner
= basepositioner
278 self
.vmin
= subaxis
.vmin
279 self
.vmax
= subaxis
.vmax
280 self
.vminover
= subaxis
.vminover
281 self
.vmaxover
= subaxis
.vmaxover
283 def vbasepath(self
, v1
=None, v2
=None):
285 v1
= self
.vmin
+v1
*(self
.vmax
-self
.vmin
)
289 v2
= self
.vmin
+v2
*(self
.vmax
-self
.vmin
)
292 return self
.basepositioner
.vbasepath(v1
, v2
)
294 def vgridpath(self
, v
):
295 return self
.basepositioner
.vgridpath(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
297 def vtickpoint_pt(self
, v
, axis
=None):
298 return self
.basepositioner
.vtickpoint_pt(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
300 def vtickdirection(self
, v
, axis
=None):
301 return self
.basepositioner
.vtickdirection(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
306 def __init__(self
, subaxes
=None, defaultsubaxis
=linear(painter
=None, linkpainter
=None, parter
=None),
307 dist
=0.5, firstdist
=None, lastdist
=None, title
=None, reverse
=0,
308 painter
=painter
.bar(), linkpainter
=painter
.linkedbar()):
309 self
.subaxes
= subaxes
310 self
.defaultsubaxis
= defaultsubaxis
312 if firstdist
is not None:
313 self
.firstdist
= firstdist
315 self
.firstdist
= 0.5 * dist
316 if lastdist
is not None:
317 self
.lastdist
= lastdist
319 self
.lastdist
= 0.5 * dist
321 self
.reverse
= reverse
322 self
.painter
= painter
323 self
.linkpainter
= linkpainter
325 def createdata(self
, errorname
):
326 data
= axisdata(size
=self
.firstdist
+self
.lastdist
-self
.dist
, subaxes
={}, names
=[])
329 def addsubaxis(self
, data
, name
, subaxis
, graphtexrunner
, errorname
):
330 subaxis
= anchoredaxis(subaxis
, graphtexrunner
, "%s, subaxis %s" % (errorname
, name
))
331 subaxis
.setcreatecall(lambda: None)
332 subaxis
.sized
= hasattr(subaxis
.data
, "size")
334 data
.size
+= subaxis
.data
.size
337 data
.size
+= self
.dist
338 data
.subaxes
[name
] = subaxis
340 data
.names
.insert(0, name
)
342 data
.names
.append(name
)
344 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
345 for value
in columndata
:
347 # some checks and error messages
351 raise ValueError("tuple expected by bar axis '%s'" % errorname
)
357 raise ValueError("tuple expected by bar axis '%s'" % errorname
)
358 assert len(value
) == 2, "tuple of size two expected by bar axis '%s'" % errorname
361 if name
is not None and name
not in data
.names
:
363 if self
.subaxes
[name
] is not None:
364 self
.addsubaxis(data
, name
, self
.subaxes
[name
], graphtexrunner
, errorname
)
366 self
.addsubaxis(data
, name
, self
.defaultsubaxis
, graphtexrunner
, errorname
)
367 for name
in data
.names
:
368 subaxis
= data
.subaxes
[name
]
370 data
.size
-= subaxis
.data
.size
371 subaxis
.axis
.adjustaxis(subaxis
.data
,
372 [value
[1] for value
in columndata
if value
[0] == name
],
374 "%s, subaxis %s" % (errorname
, name
))
376 data
.size
+= subaxis
.data
.size
378 def convert(self
, data
, value
):
381 axis
= data
.subaxes
[value
[0]]
384 return axis
.vmin
+ axis
.convert(value
[1]) * (axis
.vmax
- axis
.vmin
)
386 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
387 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
389 position
= self
.firstdist
390 for name
in data
.names
:
391 subaxis
= data
.subaxes
[name
]
392 subaxis
.vmin
= position
/ float(data
.size
)
394 position
+= subaxis
.data
.size
397 subaxis
.vmax
= position
/ float(data
.size
)
398 position
+= 0.5*self
.dist
400 if name
== data
.names
[-1]:
403 subaxis
.vmaxover
= position
/ float(data
.size
)
404 subaxis
.setpositioner(subaxispositioner(positioner
, subaxis
))
406 canvas
.insert(subaxis
.canvas
)
407 if canvas
.extent_pt
< subaxis
.canvas
.extent_pt
:
408 canvas
.extent_pt
= subaxis
.canvas
.extent_pt
409 position
+= 0.5*self
.dist
411 if self
.painter
is not None:
412 self
.painter
.paint(canvas
, data
, self
, positioner
)
415 def createlinked(self
, data
, positioner
, graphtexrunner
, errorname
, linkpainter
):
416 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
417 for name
in data
.names
:
418 subaxis
= data
.subaxes
[name
]
419 subaxis
= linkedaxis(subaxis
, name
)
420 subaxis
.setpositioner(subaxispositioner(positioner
, data
.subaxes
[name
]))
422 canvas
.insert(subaxis
.canvas
)
423 if canvas
.extent_pt
< subaxis
.canvas
.extent_pt
:
424 canvas
.extent_pt
= subaxis
.canvas
.extent_pt
425 if linkpainter
is not None:
426 linkpainter
.paint(canvas
, data
, self
, positioner
)
430 class nestedbar(bar
):
432 def __init__(self
, defaultsubaxis
=bar(dist
=0, painter
=None, linkpainter
=None), **kwargs
):
433 bar
.__init
__(self
, defaultsubaxis
=defaultsubaxis
, **kwargs
)
438 def __init__(self
, defaultsubaxis
=linear(),
439 firstdist
=0, lastdist
=0,
440 painter
=painter
.split(), linkpainter
=painter
.linkedsplit(), **kwargs
):
441 bar
.__init
__(self
, defaultsubaxis
=defaultsubaxis
,
442 firstdist
=firstdist
, lastdist
=lastdist
,
443 painter
=painter
, linkpainter
=linkpainter
, **kwargs
)
446 class sizedlinear(linear
):
448 def __init__(self
, size
=1, **kwargs
):
449 linear
.__init
__(self
, **kwargs
)
452 def createdata(self
, errorname
):
453 data
= linear
.createdata(self
, errorname
)
454 data
.size
= self
.size
457 sizedlin
= sizedlinear
460 class autosizedlinear(linear
):
462 def __init__(self
, parter
=parter
.autolinear(extendtick
=None), **kwargs
):
463 linear
.__init
__(self
, parter
=parter
, **kwargs
)
465 def createdata(self
, errorname
):
466 data
= linear
.createdata(self
, errorname
)
468 data
.size
= data
.max - data
.min
473 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
474 linear
.adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
)
476 data
.size
= data
.max - data
.min
480 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
483 canvas
= linear
.create(self
, data
, positioner
, graphtexrunner
, errorname
)
484 if min != data
.min or max != data
.max:
485 raise RuntimeError("range change during axis creation of autosized linear axis")
488 autosizedlin
= autosizedlinear
493 def __init__(self
, axis
, graphtexrunner
, errorname
):
494 assert not isinstance(axis
, anchoredaxis
), errorname
496 self
.errorname
= errorname
497 self
.graphtexrunner
= graphtexrunner
498 self
.data
= axis
.createdata(self
.errorname
)
500 self
.positioner
= None
502 def setcreatecall(self
, function
, *args
, **kwargs
):
503 self
._createfunction
= function
504 self
._createargs
= args
505 self
._createkwargs
= kwargs
509 self
._createfunction
(*self
._createargs
, **self
._createkwargs
)
511 def setpositioner(self
, positioner
):
512 assert positioner
is not None, self
.errorname
513 assert self
.positioner
is None, self
.errorname
514 self
.positioner
= positioner
516 def convert(self
, x
):
518 return self
.axis
.convert(self
.data
, x
)
520 def adjustaxis(self
, columndata
):
521 if self
.canvas
is None:
522 self
.axis
.adjustaxis(self
.data
, columndata
, self
.graphtexrunner
, self
.errorname
)
524 warnings
.warn("ignore axis range adjustment of already created axis '%s'" % self
.errorname
)
526 def vbasepath(self
, v1
=None, v2
=None):
527 return self
.positioner
.vbasepath(v1
=v1
, v2
=v2
)
529 def basepath(self
, x1
=None, x2
=None):
533 return self
.positioner
.vbasepath()
535 return self
.positioner
.vbasepath(v2
=self
.axis
.convert(self
.data
, x2
))
538 return self
.positioner
.vbasepath(v1
=self
.axis
.convert(self
.data
, x1
))
540 return self
.positioner
.vbasepath(v1
=self
.axis
.convert(self
.data
, x1
),
541 v2
=self
.axis
.convert(self
.data
, x2
))
543 def vgridpath(self
, v
):
544 return self
.positioner
.vgridpath(v
)
546 def gridpath(self
, x
):
548 return self
.positioner
.vgridpath(self
.axis
.convert(self
.data
, x
))
550 def vtickpoint_pt(self
, v
):
551 return self
.positioner
.vtickpoint_pt(v
)
553 def vtickpoint(self
, v
):
554 return self
.positioner
.vtickpoint_pt(v
) * unit
.t_pt
556 def tickpoint_pt(self
, x
):
558 return self
.positioner
.vtickpoint_pt(self
.axis
.convert(self
.data
, x
))
560 def tickpoint(self
, x
):
562 x_pt
, y_pt
= self
.positioner
.vtickpoint_pt(self
.axis
.convert(self
.data
, x
))
563 return x_pt
* unit
.t_pt
, y_pt
* unit
.t_pt
565 def vtickdirection(self
, v
):
566 return self
.positioner
.vtickdirection(v
)
568 def tickdirection(self
, x
):
570 return self
.positioner
.vtickdirection(self
.axis
.convert(self
.data
, x
))
573 if self
.canvas
is None:
574 assert self
.positioner
is not None, self
.errorname
575 self
.canvas
= self
.axis
.create(self
.data
, self
.positioner
, self
.graphtexrunner
, self
.errorname
)
579 class linkedaxis(anchoredaxis
):
581 def __init__(self
, linkedaxis
=None, errorname
="manual-linked", painter
=_marker
):
582 self
.painter
= painter
584 self
.errorname
= errorname
586 self
.positioner
= None
588 self
.setlinkedaxis(linkedaxis
)
590 def setlinkedaxis(self
, linkedaxis
):
591 assert isinstance(linkedaxis
, anchoredaxis
), self
.errorname
592 self
.linkedto
= linkedaxis
593 self
.axis
= linkedaxis
.axis
594 self
.graphtexrunner
= self
.linkedto
.graphtexrunner
595 self
.errorname
= "%s (linked to %s)" % (self
.errorname
, linkedaxis
.errorname
)
596 self
.data
= linkedaxis
.data
597 if self
.painter
is _marker
:
598 self
.painter
= linkedaxis
.axis
.linkpainter
601 assert self
.linkedto
is not None, self
.errorname
602 assert self
.positioner
is not None, self
.errorname
603 if self
.canvas
is None:
604 self
.linkedto
.docreate()
605 self
.canvas
= self
.axis
.createlinked(self
.data
, self
.positioner
, self
.graphtexrunner
, self
.errorname
, self
.painter
)
609 class anchoredpathaxis(anchoredaxis
):
610 """an anchored axis along a path"""
612 def __init__(self
, path
, axis
, **kwargs
):
613 anchoredaxis
.__init
__(self
, axis
, text
.defaulttexrunner
, "pathaxis")
614 self
.setpositioner(positioner
.pathpositioner(path
, **kwargs
))
617 def pathaxis(*args
, **kwargs
):
618 """creates an axiscanvas for an axis along a path"""
619 return anchoredpathaxis(*args
, **kwargs
).canvas