1 # -*- coding: ISO-8859-1 -*-
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-2006 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 partfunctions
= parter
.partfunctions(data
.min/self
.divisor
, data
.max/self
.divisor
,
159 self
.min is None, self
.max is None)
161 partfunctions
= parter
.partfunctions(data
.min, data
.max,
162 self
.min is None, self
.max is None)
164 for partfunction
in partfunctions
:
166 while worse
< self
.maxworse
:
168 ticks
= partfunction()
171 ticks
= [t
for t
in tick
.mergeticklists(self
.manualticks
, ticks
, mergeequal
=0)
172 if t
.ticklevel
is not None or t
.labellevel
is not None]
174 rate
= rater
.rateticks(self
, ticks
, self
.density
)
176 rate
+= rater
.raterange(self
.convert(data
, ticks
[0]) -
177 self
.convert(data
, ticks
[-1]), 1)
179 rate
+= rater
.raterange(self
.convert(data
, ticks
[-1]) -
180 self
.convert(data
, ticks
[0]), 1)
181 if bestrate
is None or rate
< bestrate
:
184 variants
.append(variant(data
, rate
=rate
, ticks
=ticks
))
187 raise RuntimeError("no axis partitioning found%s" % errorname
)
189 if len(variants
) == 1 or self
.painter
is None:
190 # When the painter is None, we could sort the variants here by their rating.
191 # However, we didn't did this so far and there is no real reason to change that.
192 data
.ticks
= variants
[0].ticks
195 # build the layout for best variants
196 for variant
in variants
:
197 variant
.storedcanvas
= None
199 while not variants
[0].storedcanvas
:
200 variants
[0].storedcanvas
= layout(variants
[0])
201 ratelayout
= rater
.ratelayout(variants
[0].storedcanvas
, self
.density
)
202 if ratelayout
is None:
205 raise NoValidPartitionError("no valid axis partitioning found%s" % errorname
)
207 variants
[0].rate
+= ratelayout
209 self
.adjustaxis(data
, variants
[0].ticks
, graphtexrunner
, errorname
)
210 data
.ticks
= variants
[0].ticks
211 return variants
[0].storedcanvas
214 class linear(_regularaxis
):
217 def __init__(self
, parter
=parter
.autolinear(), rater
=rater
.linear(), **args
):
218 _regularaxis
.__init
__(self
, **args
)
222 def convert(self
, data
, value
):
223 """axis coordinates -> graph coordinates"""
225 return (data
.max - float(value
)) / (data
.max - data
.min)
227 return (float(value
) - data
.min) / (data
.max - data
.min)
229 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
230 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.parter
, self
.rater
, errorname
)
235 class logarithmic(_regularaxis
):
236 """logarithmic axis"""
238 def __init__(self
, parter
=parter
.autologarithmic(), rater
=rater
.logarithmic(),
239 linearparter
=parter
.autolinear(extendtick
=None), **args
):
240 _regularaxis
.__init
__(self
, **args
)
243 self
.linearparter
= linearparter
245 def convert(self
, data
, value
):
246 """axis coordinates -> graph coordinates"""
247 # TODO: store log(data.min) and log(data.max)
249 return (math
.log(data
.max) - math
.log(float(value
))) / (math
.log(data
.max) - math
.log(data
.min))
251 return (math
.log(float(value
)) - math
.log(data
.min)) / (math
.log(data
.max) - math
.log(data
.min))
253 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
255 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.parter
, self
.rater
, errorname
)
256 except NoValidPartitionError
:
257 if self
.linearparter
:
258 warnings
.warn("no valid logarithmic partitioning found for axis %s, switch to linear partitioning" % errorname
)
259 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.linearparter
, self
.rater
, errorname
)
265 class subaxispositioner(positioner
._positioner
):
266 """a subaxis positioner"""
268 def __init__(self
, basepositioner
, subaxis
):
269 self
.basepositioner
= basepositioner
270 self
.vmin
= subaxis
.vmin
271 self
.vmax
= subaxis
.vmax
272 self
.vminover
= subaxis
.vminover
273 self
.vmaxover
= subaxis
.vmaxover
275 def vbasepath(self
, v1
=None, v2
=None):
277 v1
= self
.vmin
+v1
*(self
.vmax
-self
.vmin
)
281 v2
= self
.vmin
+v2
*(self
.vmax
-self
.vmin
)
284 return self
.basepositioner
.vbasepath(v1
, v2
)
286 def vgridpath(self
, v
):
287 return self
.basepositioner
.vgridpath(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
289 def vtickpoint_pt(self
, v
, axis
=None):
290 return self
.basepositioner
.vtickpoint_pt(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
292 def vtickdirection(self
, v
, axis
=None):
293 return self
.basepositioner
.vtickdirection(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
298 def __init__(self
, subaxes
=None, defaultsubaxis
=linear(painter
=None, linkpainter
=None, parter
=None),
299 dist
=0.5, firstdist
=None, lastdist
=None, title
=None, reverse
=0,
300 painter
=painter
.bar(), linkpainter
=painter
.linkedbar()):
301 self
.subaxes
= subaxes
302 self
.defaultsubaxis
= defaultsubaxis
304 if firstdist
is not None:
305 self
.firstdist
= firstdist
307 self
.firstdist
= 0.5 * dist
308 if lastdist
is not None:
309 self
.lastdist
= lastdist
311 self
.lastdist
= 0.5 * dist
313 self
.reverse
= reverse
314 self
.painter
= painter
315 self
.linkpainter
= linkpainter
317 def createdata(self
, errorname
):
318 data
= axisdata(size
=self
.firstdist
+self
.lastdist
-self
.dist
, subaxes
={}, names
=[])
321 def addsubaxis(self
, data
, name
, subaxis
, graphtexrunner
, errorname
):
322 subaxis
= anchoredaxis(subaxis
, graphtexrunner
, "%s, subaxis %s" % (errorname
, name
))
323 subaxis
.setcreatecall(lambda: None)
324 subaxis
.sized
= hasattr(subaxis
.data
, "size")
326 data
.size
+= subaxis
.data
.size
329 data
.size
+= self
.dist
330 data
.subaxes
[name
] = subaxis
332 data
.names
.insert(0, name
)
334 data
.names
.append(name
)
336 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
337 for value
in columndata
:
339 # some checks and error messages
343 raise ValueError("tuple expected by bar axis '%s'" % errorname
)
349 raise ValueError("tuple expected by bar axis '%s'" % errorname
)
350 assert len(value
) == 2, "tuple of size two expected by bar axis '%s'" % errorname
353 if name
is not None and name
not in data
.names
:
355 if self
.subaxes
[name
] is not None:
356 self
.addsubaxis(data
, name
, self
.subaxes
[name
], graphtexrunner
, errorname
)
358 self
.addsubaxis(data
, name
, self
.defaultsubaxis
, graphtexrunner
, errorname
)
359 for name
in data
.names
:
360 subaxis
= data
.subaxes
[name
]
362 data
.size
-= subaxis
.data
.size
363 subaxis
.axis
.adjustaxis(subaxis
.data
,
364 [value
[1] for value
in columndata
if value
[0] == name
],
366 "%s, subaxis %s" % (errorname
, name
))
368 data
.size
+= subaxis
.data
.size
370 def convert(self
, data
, value
):
373 axis
= data
.subaxes
[value
[0]]
376 return axis
.vmin
+ axis
.convert(value
[1]) * (axis
.vmax
- axis
.vmin
)
378 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
379 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
381 position
= self
.firstdist
382 for name
in data
.names
:
383 subaxis
= data
.subaxes
[name
]
384 subaxis
.vmin
= position
/ float(data
.size
)
386 position
+= subaxis
.data
.size
389 subaxis
.vmax
= position
/ float(data
.size
)
390 position
+= 0.5*self
.dist
392 if name
== data
.names
[-1]:
395 subaxis
.vmaxover
= position
/ float(data
.size
)
396 subaxis
.setpositioner(subaxispositioner(positioner
, subaxis
))
398 canvas
.insert(subaxis
.canvas
)
399 if canvas
.extent_pt
< subaxis
.canvas
.extent_pt
:
400 canvas
.extent_pt
= subaxis
.canvas
.extent_pt
401 position
+= 0.5*self
.dist
403 if self
.painter
is not None:
404 self
.painter
.paint(canvas
, data
, self
, positioner
)
407 def createlinked(self
, data
, positioner
, graphtexrunner
, errorname
, linkpainter
):
408 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
409 for name
in data
.names
:
410 subaxis
= data
.subaxes
[name
]
411 subaxis
= linkedaxis(subaxis
, name
)
412 subaxis
.setpositioner(subaxispositioner(positioner
, data
.subaxes
[name
]))
414 canvas
.insert(subaxis
.canvas
)
415 if canvas
.extent_pt
< subaxis
.canvas
.extent_pt
:
416 canvas
.extent_pt
= subaxis
.canvas
.extent_pt
417 if linkpainter
is not None:
418 linkpainter
.paint(canvas
, data
, self
, positioner
)
422 class nestedbar(bar
):
424 def __init__(self
, defaultsubaxis
=bar(dist
=0, painter
=None, linkpainter
=None), **kwargs
):
425 bar
.__init
__(self
, defaultsubaxis
=defaultsubaxis
, **kwargs
)
430 def __init__(self
, defaultsubaxis
=linear(),
431 firstdist
=0, lastdist
=0,
432 painter
=painter
.split(), linkpainter
=painter
.linkedsplit(), **kwargs
):
433 bar
.__init
__(self
, defaultsubaxis
=defaultsubaxis
,
434 firstdist
=firstdist
, lastdist
=lastdist
,
435 painter
=painter
, linkpainter
=linkpainter
, **kwargs
)
438 class sizedlinear(linear
):
440 def __init__(self
, size
=1, **kwargs
):
441 linear
.__init
__(self
, **kwargs
)
444 def createdata(self
, errorname
):
445 data
= linear
.createdata(self
, errorname
)
446 data
.size
= self
.size
449 sizedlin
= sizedlinear
452 class autosizedlinear(linear
):
454 def __init__(self
, parter
=parter
.autolinear(extendtick
=None), **kwargs
):
455 linear
.__init
__(self
, parter
=parter
, **kwargs
)
457 def createdata(self
, errorname
):
458 data
= linear
.createdata(self
, errorname
)
460 data
.size
= data
.max - data
.min
465 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
466 linear
.adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
)
468 data
.size
= data
.max - data
.min
472 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
475 canvas
= linear
.create(self
, data
, positioner
, graphtexrunner
, errorname
)
476 if min != data
.min or max != data
.max:
477 raise RuntimeError("range change during axis creation of autosized linear axis")
480 autosizedlin
= autosizedlinear
485 def __init__(self
, axis
, graphtexrunner
, errorname
):
486 assert not isinstance(axis
, anchoredaxis
), errorname
488 self
.errorname
= errorname
489 self
.graphtexrunner
= graphtexrunner
490 self
.data
= axis
.createdata(self
.errorname
)
492 self
.positioner
= None
494 def setcreatecall(self
, function
, *args
, **kwargs
):
495 self
._createfunction
= function
496 self
._createargs
= args
497 self
._createkwargs
= kwargs
501 self
._createfunction
(*self
._createargs
, **self
._createkwargs
)
503 def setpositioner(self
, positioner
):
504 assert positioner
is not None, self
.errorname
505 assert self
.positioner
is None, self
.errorname
506 self
.positioner
= positioner
508 def convert(self
, x
):
510 return self
.axis
.convert(self
.data
, x
)
512 def adjustaxis(self
, columndata
):
513 if self
.canvas
is None:
514 self
.axis
.adjustaxis(self
.data
, columndata
, self
.graphtexrunner
, self
.errorname
)
516 warnings
.warn("ignore axis range adjustment of already created axis '%s'" % self
.errorname
)
518 def vbasepath(self
, v1
=None, v2
=None):
519 return self
.positioner
.vbasepath(v1
=v1
, v2
=v2
)
521 def basepath(self
, x1
=None, x2
=None):
525 return self
.positioner
.vbasepath()
527 return self
.positioner
.vbasepath(v2
=self
.axis
.convert(self
.data
, x2
))
530 return self
.positioner
.vbasepath(v1
=self
.axis
.convert(self
.data
, x1
))
532 return self
.positioner
.vbasepath(v1
=self
.axis
.convert(self
.data
, x1
),
533 v2
=self
.axis
.convert(self
.data
, x2
))
535 def vgridpath(self
, v
):
536 return self
.positioner
.vgridpath(v
)
538 def gridpath(self
, x
):
540 return self
.positioner
.vgridpath(self
.axis
.convert(self
.data
, x
))
542 def vtickpoint_pt(self
, v
):
543 return self
.positioner
.vtickpoint_pt(v
)
545 def vtickpoint(self
, v
):
546 return self
.positioner
.vtickpoint_pt(v
) * unit
.t_pt
548 def tickpoint_pt(self
, x
):
550 return self
.positioner
.vtickpoint_pt(self
.axis
.convert(self
.data
, x
))
552 def tickpoint(self
, x
):
554 x_pt
, y_pt
= self
.positioner
.vtickpoint_pt(self
.axis
.convert(self
.data
, x
))
555 return x_pt
* unit
.t_pt
, y_pt
* unit
.t_pt
557 def vtickdirection(self
, v
):
558 return self
.positioner
.vtickdirection(v
)
560 def tickdirection(self
, x
):
562 return self
.positioner
.vtickdirection(self
.axis
.convert(self
.data
, x
))
565 if self
.canvas
is None:
566 assert self
.positioner
is not None, self
.errorname
567 self
.canvas
= self
.axis
.create(self
.data
, self
.positioner
, self
.graphtexrunner
, self
.errorname
)
571 class linkedaxis(anchoredaxis
):
573 def __init__(self
, linkedaxis
=None, errorname
="manual-linked", painter
=_marker
):
574 self
.painter
= painter
576 self
.errorname
= errorname
578 self
.positioner
= None
580 self
.setlinkedaxis(linkedaxis
)
582 def setlinkedaxis(self
, linkedaxis
):
583 assert isinstance(linkedaxis
, anchoredaxis
), self
.errorname
584 self
.linkedto
= linkedaxis
585 self
.axis
= linkedaxis
.axis
586 self
.graphtexrunner
= self
.linkedto
.graphtexrunner
587 self
.errorname
= "%s (linked to %s)" % (self
.errorname
, linkedaxis
.errorname
)
588 self
.data
= linkedaxis
.data
589 if self
.painter
is _marker
:
590 self
.painter
= linkedaxis
.axis
.linkpainter
593 assert self
.linkedto
is not None, self
.errorname
594 assert self
.positioner
is not None, self
.errorname
595 if self
.canvas
is None:
596 self
.linkedto
.docreate()
597 self
.canvas
= self
.axis
.createlinked(self
.data
, self
.positioner
, self
.graphtexrunner
, self
.errorname
, self
.painter
)
601 class anchoredpathaxis(anchoredaxis
):
602 """an anchored axis along a path"""
604 def __init__(self
, path
, axis
, **kwargs
):
605 anchoredaxis
.__init
__(self
, axis
, text
.defaulttexrunner
, "pathaxis")
606 self
.setpositioner(positioner
.pathpositioner(path
, **kwargs
))
609 def pathaxis(*args
, **kwargs
):
610 """creates an axiscanvas for an axis along a path"""
611 return anchoredpathaxis(*args
, **kwargs
).canvas