1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2002-2012 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2011 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2012 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
24 import functools
, logging
, math
25 from pyx
import attr
, unit
, text
26 from pyx
.graph
.axis
import painter
, parter
, positioner
, rater
, texter
, tick
28 logger
= logging
.getLogger("pyx")
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 list(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
)
126 if self
.divisor
is not None:
127 rational_divisor
= tick
.rational(self
.divisor
)
128 convert_tick
= lambda x
: float(x
)*self
.divisor
130 convert_tick
= lambda x
: x
134 self
.adjustaxis(data
, [convert_tick(data
.ticks
[0]), convert_tick(data
.ticks
[-1])], graphtexrunner
, errorname
)
135 self
.texter
.labels(data
.ticks
)
138 t
*= rational_divisor
139 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
140 if self
.painter
is not None:
141 self
.painter
.paint(canvas
, data
, self
, positioner
)
145 data
.ticks
= self
.manualticks
148 # a variant is a data copy with local modifications to test several partitions
149 @functools.total_ordering
151 def __init__(self
, data
, **kwargs
):
153 for key
, value
in list(kwargs
.items()):
154 setattr(self
, key
, value
)
156 def __getattr__(self
, key
):
157 return getattr(data
, key
)
159 def __lt__(self
, other
):
160 # we can also sort variants by their rate
161 return self
.rate
< other
.rate
163 def __eq__(self
, other
):
164 # we can also sort variants by their rate
165 return self
.rate
== other
.rate
167 # build a list of variants
169 if self
.divisor
is not None:
170 if data
.min is not None:
171 data_min_divided
= data
.min/self
.divisor
173 data_min_divided
= None
174 if data
.max is not None:
175 data_max_divided
= data
.max/self
.divisor
177 data_max_divided
= None
178 partfunctions
= parter
.partfunctions(data_min_divided
, data_max_divided
,
179 self
.min is None, self
.max is None)
181 partfunctions
= parter
.partfunctions(data
.min, data
.max,
182 self
.min is None, self
.max is None)
184 for partfunction
in partfunctions
:
186 while worse
< self
.maxworse
:
188 ticks
= partfunction()
191 ticks
= tick
.mergeticklists(self
.manualticks
, ticks
, mergeequal
=0)
193 rate
= rater
.rateticks(self
, ticks
, self
.density
)
196 rate
+= rater
.raterange(self
.convert(data
, convert_tick(ticks
[0])) -
197 self
.convert(data
, convert_tick(ticks
[-1])), 1)
199 rate
+= rater
.raterange(self
.convert(data
, convert_tick(ticks
[-1])) -
200 self
.convert(data
, convert_tick(ticks
[0])), 1)
201 if bestrate
is None or rate
< bestrate
:
204 variants
.append(variant(data
, rate
=rate
, ticks
=ticks
))
207 raise RuntimeError("no axis partitioning found%s" % errorname
)
209 if len(variants
) == 1 or self
.painter
is None:
210 # When the painter is None, we could sort the variants here by their rating.
211 # However, we didn't did this so far and there is no real reason to change that.
212 data
.ticks
= variants
[0].ticks
215 # build the layout for best variants
216 for variant
in variants
:
217 variant
.storedcanvas
= None
219 while not variants
[0].storedcanvas
:
220 variants
[0].storedcanvas
= layout(variants
[0])
221 ratelayout
= rater
.ratelayout(variants
[0].storedcanvas
, self
.density
)
222 if ratelayout
is None:
225 raise NoValidPartitionError("no valid axis partitioning found%s" % errorname
)
227 variants
[0].rate
+= ratelayout
229 self
.adjustaxis(data
, variants
[0].ticks
, graphtexrunner
, errorname
)
230 data
.ticks
= variants
[0].ticks
231 return variants
[0].storedcanvas
234 class linear(_regularaxis
):
237 def __init__(self
, parter
=parter
.autolinear(), rater
=rater
.linear(), **args
):
238 _regularaxis
.__init
__(self
, **args
)
242 def convert(self
, data
, value
):
243 """axis coordinates -> graph coordinates"""
245 return (data
.max - float(value
)) / (data
.max - data
.min)
247 return (float(value
) - data
.min) / (data
.max - data
.min)
249 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
250 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.parter
, self
.rater
, errorname
)
255 class logarithmic(_regularaxis
):
256 """logarithmic axis"""
258 def __init__(self
, parter
=parter
.autologarithmic(), rater
=rater
.logarithmic(),
259 linearparter
=parter
.autolinear(extendtick
=None), **args
):
260 _regularaxis
.__init
__(self
, **args
)
263 self
.linearparter
= linearparter
265 def convert(self
, data
, value
):
266 """axis coordinates -> graph coordinates"""
267 # TODO: store log(data.min) and log(data.max)
269 return (math
.log(data
.max) - math
.log(float(value
))) / (math
.log(data
.max) - math
.log(data
.min))
271 return (math
.log(float(value
)) - math
.log(data
.min)) / (math
.log(data
.max) - math
.log(data
.min))
273 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
275 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.parter
, self
.rater
, errorname
)
276 except NoValidPartitionError
:
277 if self
.linearparter
:
278 logger
.warning("no valid logarithmic partitioning found for axis %s, switch to linear partitioning" % errorname
)
279 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.linearparter
, self
.rater
, errorname
)
285 class subaxispositioner(positioner
._positioner
):
286 """a subaxis positioner"""
288 def __init__(self
, basepositioner
, subaxis
):
289 self
.basepositioner
= basepositioner
290 self
.vmin
= subaxis
.vmin
291 self
.vmax
= subaxis
.vmax
292 self
.vminover
= subaxis
.vminover
293 self
.vmaxover
= subaxis
.vmaxover
295 def vbasepath(self
, v1
=None, v2
=None):
297 v1
= self
.vmin
+v1
*(self
.vmax
-self
.vmin
)
301 v2
= self
.vmin
+v2
*(self
.vmax
-self
.vmin
)
304 return self
.basepositioner
.vbasepath(v1
, v2
)
306 def vgridpath(self
, v
):
307 return self
.basepositioner
.vgridpath(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
309 def vtickpoint_pt(self
, v
, axis
=None):
310 return self
.basepositioner
.vtickpoint_pt(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
312 def vtickdirection(self
, v
, axis
=None):
313 return self
.basepositioner
.vtickdirection(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
318 def __init__(self
, subaxes
=None, defaultsubaxis
=linear(painter
=None, linkpainter
=None, parter
=None),
319 dist
=0.5, firstdist
=None, lastdist
=None, title
=None, reverse
=0,
320 painter
=painter
.bar(), linkpainter
=painter
.linkedbar()):
321 self
.subaxes
= subaxes
322 self
.defaultsubaxis
= defaultsubaxis
324 if firstdist
is not None:
325 self
.firstdist
= firstdist
327 self
.firstdist
= 0.5 * dist
328 if lastdist
is not None:
329 self
.lastdist
= lastdist
331 self
.lastdist
= 0.5 * dist
333 self
.reverse
= reverse
334 self
.painter
= painter
335 self
.linkpainter
= linkpainter
337 def createdata(self
, errorname
):
338 data
= axisdata(size
=self
.firstdist
+self
.lastdist
-self
.dist
, subaxes
={}, names
=[])
341 def addsubaxis(self
, data
, name
, subaxis
, graphtexrunner
, errorname
):
342 subaxis
= anchoredaxis(subaxis
, graphtexrunner
, "%s, subaxis %s" % (errorname
, name
))
343 subaxis
.setcreatecall(lambda: None)
344 subaxis
.sized
= hasattr(subaxis
.data
, "size")
346 data
.size
+= subaxis
.data
.size
349 data
.size
+= self
.dist
350 data
.subaxes
[name
] = subaxis
352 data
.names
.insert(0, name
)
354 data
.names
.append(name
)
356 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
357 for value
in columndata
:
359 # some checks and error messages
363 raise ValueError("tuple expected by bar axis '%s'" % errorname
)
369 raise ValueError("tuple expected by bar axis '%s'" % errorname
)
370 assert len(value
) == 2, "tuple of size two expected by bar axis '%s'" % errorname
373 if name
is not None and name
not in data
.names
:
375 if self
.subaxes
[name
] is not None:
376 self
.addsubaxis(data
, name
, self
.subaxes
[name
], graphtexrunner
, errorname
)
378 self
.addsubaxis(data
, name
, self
.defaultsubaxis
, graphtexrunner
, errorname
)
379 for name
in data
.names
:
380 subaxis
= data
.subaxes
[name
]
382 data
.size
-= subaxis
.data
.size
383 subaxis
.axis
.adjustaxis(subaxis
.data
,
384 [value
[1] for value
in columndata
if value
[0] == name
],
386 "%s, subaxis %s" % (errorname
, name
))
388 data
.size
+= subaxis
.data
.size
390 def convert(self
, data
, value
):
393 axis
= data
.subaxes
[value
[0]]
396 return axis
.vmin
+ axis
.convert(value
[1]) * (axis
.vmax
- axis
.vmin
)
398 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
399 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
401 position
= self
.firstdist
402 for name
in data
.names
:
403 subaxis
= data
.subaxes
[name
]
404 subaxis
.vmin
= position
/ float(data
.size
)
406 position
+= subaxis
.data
.size
409 subaxis
.vmax
= position
/ float(data
.size
)
410 position
+= 0.5*self
.dist
412 if name
== data
.names
[-1]:
415 subaxis
.vmaxover
= position
/ float(data
.size
)
416 subaxis
.setpositioner(subaxispositioner(positioner
, subaxis
))
418 for layer
, subcanvas
in list(subaxis
.canvas
.layers
.items()):
419 canvas
.layer(layer
).insert(subcanvas
)
420 assert len(subaxis
.canvas
.layers
) == len(subaxis
.canvas
.items
)
421 if canvas
.extent_pt
< subaxis
.canvas
.extent_pt
:
422 canvas
.extent_pt
= subaxis
.canvas
.extent_pt
423 position
+= 0.5*self
.dist
425 if self
.painter
is not None:
426 self
.painter
.paint(canvas
, data
, self
, positioner
)
429 def createlinked(self
, data
, positioner
, graphtexrunner
, errorname
, linkpainter
):
430 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
431 for name
in data
.names
:
432 subaxis
= data
.subaxes
[name
]
433 subaxis
= linkedaxis(subaxis
, name
)
434 subaxis
.setpositioner(subaxispositioner(positioner
, data
.subaxes
[name
]))
436 for layer
, subcanvas
in list(subaxis
.canvas
.layers
.items()):
437 canvas
.layer(layer
).insert(subcanvas
)
438 assert len(subaxis
.canvas
.layers
) == len(subaxis
.canvas
.items
)
439 if canvas
.extent_pt
< subaxis
.canvas
.extent_pt
:
440 canvas
.extent_pt
= subaxis
.canvas
.extent_pt
441 if linkpainter
is not None:
442 linkpainter
.paint(canvas
, data
, self
, positioner
)
446 class nestedbar(bar
):
448 def __init__(self
, defaultsubaxis
=bar(dist
=0, painter
=None, linkpainter
=None), **kwargs
):
449 bar
.__init
__(self
, defaultsubaxis
=defaultsubaxis
, **kwargs
)
454 def __init__(self
, defaultsubaxis
=linear(),
455 firstdist
=0, lastdist
=0,
456 painter
=painter
.split(), linkpainter
=painter
.linkedsplit(), **kwargs
):
457 bar
.__init
__(self
, defaultsubaxis
=defaultsubaxis
,
458 firstdist
=firstdist
, lastdist
=lastdist
,
459 painter
=painter
, linkpainter
=linkpainter
, **kwargs
)
462 class sizedlinear(linear
):
464 def __init__(self
, size
=1, **kwargs
):
465 linear
.__init
__(self
, **kwargs
)
468 def createdata(self
, errorname
):
469 data
= linear
.createdata(self
, errorname
)
470 data
.size
= self
.size
473 sizedlin
= sizedlinear
476 class autosizedlinear(linear
):
478 def __init__(self
, parter
=parter
.autolinear(extendtick
=None), **kwargs
):
479 linear
.__init
__(self
, parter
=parter
, **kwargs
)
481 def createdata(self
, errorname
):
482 data
= linear
.createdata(self
, errorname
)
484 data
.size
= data
.max - data
.min
489 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
490 linear
.adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
)
492 data
.size
= data
.max - data
.min
496 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
499 canvas
= linear
.create(self
, data
, positioner
, graphtexrunner
, errorname
)
500 if min != data
.min or max != data
.max:
501 raise RuntimeError("range change during axis creation of autosized linear axis")
504 autosizedlin
= autosizedlinear
509 def __init__(self
, axis
, graphtexrunner
, errorname
):
510 assert not isinstance(axis
, anchoredaxis
), errorname
512 self
.errorname
= errorname
513 self
.graphtexrunner
= graphtexrunner
514 self
.data
= axis
.createdata(self
.errorname
)
516 self
.positioner
= None
518 def setcreatecall(self
, function
, *args
, **kwargs
):
519 self
._createfunction
= function
520 self
._createargs
= args
521 self
._createkwargs
= kwargs
525 self
._createfunction
(*self
._createargs
, **self
._createkwargs
)
527 def setpositioner(self
, positioner
):
528 assert positioner
is not None, self
.errorname
529 assert self
.positioner
is None, self
.errorname
530 self
.positioner
= positioner
532 def convert(self
, x
):
534 return self
.axis
.convert(self
.data
, x
)
536 def adjustaxis(self
, columndata
):
537 if self
.canvas
is None:
538 self
.axis
.adjustaxis(self
.data
, columndata
, self
.graphtexrunner
, self
.errorname
)
540 logger
.warning("ignore axis range adjustment of already created axis '%s'" % self
.errorname
)
542 def vbasepath(self
, v1
=None, v2
=None):
543 return self
.positioner
.vbasepath(v1
=v1
, v2
=v2
)
545 def basepath(self
, x1
=None, x2
=None):
549 return self
.positioner
.vbasepath()
551 return self
.positioner
.vbasepath(v2
=self
.axis
.convert(self
.data
, x2
))
554 return self
.positioner
.vbasepath(v1
=self
.axis
.convert(self
.data
, x1
))
556 return self
.positioner
.vbasepath(v1
=self
.axis
.convert(self
.data
, x1
),
557 v2
=self
.axis
.convert(self
.data
, x2
))
559 def vgridpath(self
, v
):
560 return self
.positioner
.vgridpath(v
)
562 def gridpath(self
, x
):
564 return self
.positioner
.vgridpath(self
.axis
.convert(self
.data
, x
))
566 def vtickpoint_pt(self
, v
):
567 return self
.positioner
.vtickpoint_pt(v
)
569 def vtickpoint(self
, v
):
570 return self
.positioner
.vtickpoint_pt(v
) * unit
.t_pt
572 def tickpoint_pt(self
, x
):
574 return self
.positioner
.vtickpoint_pt(self
.axis
.convert(self
.data
, x
))
576 def tickpoint(self
, x
):
578 x_pt
, y_pt
= self
.positioner
.vtickpoint_pt(self
.axis
.convert(self
.data
, x
))
579 return x_pt
* unit
.t_pt
, y_pt
* unit
.t_pt
581 def vtickdirection(self
, v
):
582 return self
.positioner
.vtickdirection(v
)
584 def tickdirection(self
, x
):
586 return self
.positioner
.vtickdirection(self
.axis
.convert(self
.data
, x
))
589 if self
.canvas
is None:
590 assert self
.positioner
is not None, self
.errorname
591 self
.canvas
= self
.axis
.create(self
.data
, self
.positioner
, self
.graphtexrunner
, self
.errorname
)
595 class linkedaxis(anchoredaxis
):
597 def __init__(self
, linkedaxis
=None, errorname
="manual-linked", painter
=_marker
):
598 self
.painter
= painter
600 self
.errorname
= errorname
602 self
.positioner
= None
604 self
.setlinkedaxis(linkedaxis
)
606 def setlinkedaxis(self
, linkedaxis
):
607 assert isinstance(linkedaxis
, anchoredaxis
), self
.errorname
608 self
.linkedto
= linkedaxis
609 self
.axis
= linkedaxis
.axis
610 self
.graphtexrunner
= self
.linkedto
.graphtexrunner
611 self
.errorname
= "%s (linked to %s)" % (self
.errorname
, linkedaxis
.errorname
)
612 self
.data
= linkedaxis
.data
613 if self
.painter
is _marker
:
614 self
.painter
= linkedaxis
.axis
.linkpainter
617 assert self
.linkedto
is not None, self
.errorname
618 assert self
.positioner
is not None, self
.errorname
619 if self
.canvas
is None:
620 self
.linkedto
.docreate()
621 self
.canvas
= self
.axis
.createlinked(self
.data
, self
.positioner
, self
.graphtexrunner
, self
.errorname
, self
.painter
)
625 class anchoredpathaxis(anchoredaxis
):
626 """an anchored axis along a path"""
628 def __init__(self
, path
, axis
, **kwargs
):
629 anchoredaxis
.__init
__(self
, axis
, text
.defaulttexrunner
, "pathaxis")
630 self
.setpositioner(positioner
.pathpositioner(path
, **kwargs
))
633 def pathaxis(*args
, **kwargs
):
634 """creates an axiscanvas for an axis along a path"""
635 return anchoredpathaxis(*args
, **kwargs
).canvas