1 # -*- coding: ISO-8859-1 -*-
4 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2005 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 from __future__
import nested_scopes
27 from pyx
import attr
, unit
, text
28 from pyx
.graph
.axis
import painter
, parter
, positioner
, rater
, texter
, tick
33 # fallback implementation for Python 2.2 and below
35 return zip(xrange(len(list)), list)
41 """axis data storage class
43 Instances of this class are used to store axis data local to the
44 graph. It will always contain an axispos instance provided by the
45 graph during initialization."""
47 def __init__(self
, **kwargs
):
48 for key
, value
in kwargs
.items():
49 setattr(self
, key
, value
)
55 def createlinked(self
, data
, positioner
, graphtexrunner
, errorname
, linkpainter
):
56 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
57 if linkpainter
is not None:
58 linkpainter
.paint(canvas
, data
, self
, positioner
)
62 class NoValidPartitionError(RuntimeError):
67 class _regularaxis(_axis
):
68 """base implementation a regular axis
70 Regular axis have a continuous variable like linear axes,
71 logarithmic axes, time axes etc."""
73 def __init__(self
, min=None, max=None, reverse
=0, divisor
=None, title
=None,
74 painter
=painter
.regular(), texter
=texter
.mixed(), linkpainter
=painter
.linked(),
75 density
=1, maxworse
=2, manualticks
=[]):
76 if min is not None and max is not None and min > max:
77 min, max, reverse
= max, min, not reverse
80 self
.reverse
= reverse
81 self
.divisor
= divisor
83 self
.painter
= painter
85 self
.linkpainter
= linkpainter
86 self
.density
= density
87 self
.maxworse
= maxworse
88 self
.manualticks
= self
.checkfraclist(manualticks
)
90 def createdata(self
, errorname
):
91 return axisdata(min=self
.min, max=self
.max)
95 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
96 if self
.min is None or self
.max is None:
97 for value
in columndata
:
99 value
= value
+ self
.zero
103 if self
.min is None and (data
.min is None or value
< data
.min):
105 if self
.max is None and (data
.max is None or value
> data
.max):
108 def checkfraclist(self
, fracs
):
109 "orders a list of fracs, equal entries are not allowed"
110 if not len(fracs
): return []
114 for item
in sorted[1:]:
116 raise ValueError("duplicate entry found")
120 def _create(self
, data
, positioner
, graphtexrunner
, parter
, rater
, errorname
):
121 errorname
= " for axis %s" % errorname
122 if data
.min is None or data
.max is None:
123 raise RuntimeError("incomplete axis range%s" % errorname
)
126 self
.adjustaxis(data
, data
.ticks
, graphtexrunner
, errorname
)
127 self
.texter
.labels(data
.ticks
)
130 t
*= tick
.rational(self
.divisor
)
131 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
132 if self
.painter
is not None:
133 self
.painter
.paint(canvas
, data
, self
, positioner
)
137 data
.ticks
= self
.manualticks
140 # a variant is a data copy with local modifications to test several partitions
142 def __init__(self
, data
, **kwargs
):
144 for key
, value
in kwargs
.items():
145 setattr(self
, key
, value
)
147 def __getattr__(self
, key
):
148 return getattr(data
, key
)
150 def __cmp__(self
, other
):
151 # we can also sort variants by their rate
152 return cmp(self
.rate
, other
.rate
)
154 # build a list of variants
156 if self
.divisor
is not None:
157 partfunctions
= parter
.partfunctions(data
.min/self
.divisor
, data
.max/self
.divisor
,
158 self
.min is None, self
.max is None)
160 partfunctions
= parter
.partfunctions(data
.min, data
.max,
161 self
.min is None, self
.max is None)
163 for partfunction
in partfunctions
:
165 while worse
< self
.maxworse
:
167 ticks
= partfunction()
170 ticks
= tick
.mergeticklists(self
.manualticks
, ticks
, mergeequal
=0)
172 rate
= rater
.rateticks(self
, ticks
, self
.density
)
174 rater
.raterange(self
.convert(data
, ticks
[0]) -
175 self
.convert(data
, ticks
[-1]), 1)
177 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
))
185 raise RuntimeError("no axis partitioning found%s" % errorname
)
187 if len(variants
) == 1 or self
.painter
is None:
188 # When the painter is None, we could sort the variants here by their rating.
189 # However, we didn't did this so far and there is no real reason to change that.
190 data
.ticks
= variants
[0].ticks
193 # build the layout for best variants
194 for variant
in variants
:
195 variant
.storedcanvas
= None
197 while not variants
[0].storedcanvas
:
198 variants
[0].storedcanvas
= layout(variants
[0])
199 ratelayout
= rater
.ratelayout(variants
[0].storedcanvas
, self
.density
)
200 if ratelayout
is None:
203 raise NoValidPartitionError("no valid axis partitioning found%s" % errorname
)
205 variants
[0].rate
+= ratelayout
207 self
.adjustaxis(data
, variants
[0].ticks
, graphtexrunner
, errorname
)
208 data
.ticks
= variants
[0].ticks
209 return variants
[0].storedcanvas
212 class linear(_regularaxis
):
215 def __init__(self
, parter
=parter
.autolinear(), rater
=rater
.linear(), **args
):
216 _regularaxis
.__init
__(self
, **args
)
220 def convert(self
, data
, value
):
221 """axis coordinates -> graph coordinates"""
223 return (data
.max - float(value
)) / (data
.max - data
.min)
225 return (float(value
) - data
.min) / (data
.max - data
.min)
227 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
228 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.parter
, self
.rater
, errorname
)
233 class logarithmic(_regularaxis
):
234 """logarithmic axis"""
236 def __init__(self
, parter
=parter
.autologarithmic(), rater
=rater
.logarithmic(),
237 linearparter
=parter
.autolinear(extendtick
=None), **args
):
238 _regularaxis
.__init
__(self
, **args
)
241 self
.linearparter
= linearparter
243 def convert(self
, data
, value
):
244 """axis coordinates -> graph coordinates"""
245 # TODO: store log(data.min) and log(data.max)
247 return (math
.log(data
.max) - math
.log(float(value
))) / (math
.log(data
.max) - math
.log(data
.min))
249 return (math
.log(float(value
)) - math
.log(data
.min)) / (math
.log(data
.max) - math
.log(data
.min))
251 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
253 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.parter
, self
.rater
, errorname
)
254 except NoValidPartitionError
:
255 if self
.linearparter
:
256 warnings
.warn("no valid logarithmic partitioning found for axis %s, switch to linear partitioning" % errorname
)
257 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.linearparter
, self
.rater
, errorname
)
263 class subaxispositioner(positioner
._positioner
):
264 """a subaxis positioner"""
266 def __init__(self
, basepositioner
, subaxis
):
267 self
.basepositioner
= basepositioner
268 self
.vmin
= subaxis
.vmin
269 self
.vmax
= subaxis
.vmax
270 self
.vminover
= subaxis
.vminover
271 self
.vmaxover
= subaxis
.vmaxover
273 def vbasepath(self
, v1
=None, v2
=None):
275 v1
= self
.vmin
+v1
*(self
.vmax
-self
.vmin
)
279 v2
= self
.vmin
+v2
*(self
.vmax
-self
.vmin
)
282 return self
.basepositioner
.vbasepath(v1
, v2
)
284 def vgridpath(self
, v
):
285 return self
.basepositioner
.vgridpath(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
287 def vtickpoint_pt(self
, v
, axis
=None):
288 return self
.basepositioner
.vtickpoint_pt(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
290 def vtickdirection(self
, v
, axis
=None):
291 return self
.basepositioner
.vtickdirection(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
296 def __init__(self
, subaxes
=None, defaultsubaxis
=linear(painter
=None, linkpainter
=None, parter
=None),
297 dist
=0.5, firstdist
=None, lastdist
=None, title
=None, reverse
=0,
298 painter
=painter
.bar(), linkpainter
=painter
.linkedbar()):
299 self
.subaxes
= subaxes
300 self
.defaultsubaxis
= defaultsubaxis
302 if firstdist
is not None:
303 self
.firstdist
= firstdist
305 self
.firstdist
= 0.5 * dist
306 if lastdist
is not None:
307 self
.lastdist
= lastdist
309 self
.lastdist
= 0.5 * dist
311 self
.reverse
= reverse
312 self
.painter
= painter
313 self
.linkpainter
= linkpainter
315 def createdata(self
, errorname
):
316 data
= axisdata(size
=self
.firstdist
+self
.lastdist
-self
.dist
, subaxes
={}, names
=[])
319 def addsubaxis(self
, data
, name
, subaxis
, graphtexrunner
, errorname
):
320 subaxis
= anchoredaxis(subaxis
, graphtexrunner
, "%s, subaxis %s" % (errorname
, name
))
321 subaxis
.setcreatecall(lambda: None)
322 subaxis
.sized
= hasattr(subaxis
.data
, "size")
324 data
.size
+= subaxis
.data
.size
327 data
.size
+= self
.dist
328 data
.subaxes
[name
] = subaxis
330 data
.names
.insert(0, name
)
332 data
.names
.append(name
)
334 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
335 for value
in columndata
:
337 # some checks and error messages
341 raise ValueError("tuple expected by bar axis '%s'" % errorname
)
347 raise ValueError("tuple expected by bar axis '%s'" % errorname
)
348 assert len(value
) == 2, "tuple of size two expected by bar axis '%s'" % errorname
351 if name
is not None and name
not in data
.names
:
353 if self
.subaxes
[name
] is not None:
354 self
.addsubaxis(data
, name
, self
.subaxes
[name
], graphtexrunner
, errorname
)
356 self
.addsubaxis(data
, name
, self
.defaultsubaxis
, graphtexrunner
, errorname
)
357 for name
in data
.names
:
358 subaxis
= data
.subaxes
[name
]
360 data
.size
-= subaxis
.data
.size
361 subaxis
.axis
.adjustaxis(subaxis
.data
,
362 [value
[1] for value
in columndata
if value
[0] == name
],
364 "%s, subaxis %s" % (errorname
, name
))
366 data
.size
+= subaxis
.data
.size
368 def convert(self
, data
, value
):
371 axis
= data
.subaxes
[value
[0]]
374 return axis
.vmin
+ axis
.convert(value
[1]) * (axis
.vmax
- axis
.vmin
)
376 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
377 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
379 position
= self
.firstdist
380 for name
in data
.names
:
381 subaxis
= data
.subaxes
[name
]
382 subaxis
.vmin
= position
/ float(data
.size
)
384 position
+= subaxis
.data
.size
387 subaxis
.vmax
= position
/ float(data
.size
)
388 position
+= 0.5*self
.dist
390 if name
== data
.names
[-1]:
393 subaxis
.vmaxover
= position
/ float(data
.size
)
394 subaxis
.setpositioner(subaxispositioner(positioner
, subaxis
))
396 canvas
.insert(subaxis
.canvas
)
397 if canvas
.extent_pt
< subaxis
.canvas
.extent_pt
:
398 canvas
.extent_pt
= subaxis
.canvas
.extent_pt
399 position
+= 0.5*self
.dist
401 if self
.painter
is not None:
402 self
.painter
.paint(canvas
, data
, self
, positioner
)
405 def createlinked(self
, data
, positioner
, graphtexrunner
, errorname
, linkpainter
):
406 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
407 for name
in data
.names
:
408 subaxis
= data
.subaxes
[name
]
409 subaxis
= linkedaxis(subaxis
, name
)
410 subaxis
.setpositioner(subaxispositioner(positioner
, data
.subaxes
[name
]))
412 canvas
.insert(subaxis
.canvas
)
413 if canvas
.extent_pt
< subaxis
.canvas
.extent_pt
:
414 canvas
.extent_pt
= subaxis
.canvas
.extent_pt
415 if linkpainter
is not None:
416 linkpainter
.paint(canvas
, data
, self
, positioner
)
420 class nestedbar(bar
):
422 def __init__(self
, defaultsubaxis
=bar(dist
=0, painter
=None, linkpainter
=None), **kwargs
):
423 bar
.__init
__(self
, defaultsubaxis
=defaultsubaxis
, **kwargs
)
428 def __init__(self
, defaultsubaxis
=linear(),
429 firstdist
=0, lastdist
=0,
430 painter
=painter
.split(), linkpainter
=painter
.linkedsplit(), **kwargs
):
431 bar
.__init
__(self
, defaultsubaxis
=defaultsubaxis
,
432 firstdist
=firstdist
, lastdist
=lastdist
,
433 painter
=painter
, linkpainter
=linkpainter
, **kwargs
)
436 class sizedlinear(linear
):
438 def __init__(self
, size
=1, **kwargs
):
439 linear
.__init
__(self
, **kwargs
)
442 def createdata(self
, errorname
):
443 data
= linear
.createdata(self
, errorname
)
444 data
.size
= self
.size
447 sizedlin
= sizedlinear
450 class autosizedlinear(linear
):
452 def __init__(self
, parter
=parter
.autolinear(extendtick
=None), **kwargs
):
453 linear
.__init
__(self
, parter
=parter
, **kwargs
)
455 def createdata(self
, errorname
):
456 data
= linear
.createdata(self
, errorname
)
458 data
.size
= data
.max - data
.min
463 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
464 linear
.adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
)
466 data
.size
= data
.max - data
.min
470 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
473 canvas
= linear
.create(self
, data
, positioner
, graphtexrunner
, errorname
)
474 if min != data
.min or max != data
.max:
475 raise RuntimeError("range change during axis creation of autosized linear axis")
478 autosizedlin
= autosizedlinear
483 def __init__(self
, axis
, graphtexrunner
, errorname
):
484 assert not isinstance(axis
, anchoredaxis
), errorname
486 self
.errorname
= errorname
487 self
.graphtexrunner
= graphtexrunner
488 self
.data
= axis
.createdata(errorname
)
490 self
.positioner
= None
492 def setcreatecall(self
, function
, *args
, **kwargs
):
493 self
._createfunction
= function
494 self
._createargs
= args
495 self
._createkwargs
= kwargs
499 self
._createfunction
(*self
._createargs
, **self
._createkwargs
)
501 def setpositioner(self
, positioner
):
502 assert positioner
is not None, self
.errorname
503 assert self
.positioner
is None, self
.errorname
504 self
.positioner
= positioner
506 def convert(self
, x
):
508 return self
.axis
.convert(self
.data
, x
)
510 def adjustaxis(self
, columndata
):
511 if self
.canvas
is None:
512 self
.axis
.adjustaxis(self
.data
, columndata
, self
.graphtexrunner
, self
.errorname
)
514 warnings
.warn("ignore axis range adjustment of already created axis '%s'" % self
.errorname
)
516 def vbasepath(self
, v1
=None, v2
=None):
517 return self
.positioner
.vbasepath(v1
=v1
, v2
=v2
)
519 def basepath(self
, x1
=None, x2
=None):
523 return self
.positioner
.vbasepath()
525 return self
.positioner
.vbasepath(v2
=self
.axis
.convert(self
.data
, x2
))
528 return self
.positioner
.vbasepath(v1
=self
.axis
.convert(self
.data
, x1
))
530 return self
.positioner
.vbasepath(v1
=self
.axis
.convert(self
.data
, x1
),
531 v2
=self
.axis
.convert(self
.data
, x2
))
533 def vgridpath(self
, v
):
534 return self
.positioner
.vgridpath(v
)
536 def gridpath(self
, x
):
538 return self
.positioner
.vgridpath(self
.axis
.convert(self
.data
, x
))
540 def vtickpoint_pt(self
, v
):
541 return self
.positioner
.vtickpoint_pt(v
)
543 def vtickpoint(self
, v
):
544 return self
.positioner
.vtickpoint_pt(v
) * unit
.t_pt
546 def tickpoint_pt(self
, x
):
548 return self
.positioner
.vtickpoint_pt(self
.axis
.convert(self
.data
, x
))
550 def tickpoint(self
, x
):
552 x_pt
, y_pt
= self
.positioner
.vtickpoint_pt(self
.axis
.convert(self
.data
, x
))
553 return x_pt
* unit
.t_pt
, y_pt
* unit
.t_pt
555 def vtickdirection(self
, v
):
556 return self
.positioner
.vtickdirection(v
)
558 def tickdirection(self
, x
):
560 return self
.positioner
.vtickdirection(self
.axis
.convert(self
.data
, x
))
563 if self
.canvas
is None:
564 assert self
.positioner
is not None, self
.errorname
565 self
.canvas
= self
.axis
.create(self
.data
, self
.positioner
, self
.graphtexrunner
, self
.errorname
)
569 class linkedaxis(anchoredaxis
):
571 def __init__(self
, linkedaxis
=None, errorname
="manual-linked", painter
=_marker
):
572 self
.painter
= painter
574 self
.errorname
= errorname
576 self
.positioner
= None
578 self
.setlinkedaxis(linkedaxis
)
580 def setlinkedaxis(self
, linkedaxis
):
581 assert isinstance(linkedaxis
, anchoredaxis
), errorname
582 self
.linkedto
= linkedaxis
583 self
.axis
= linkedaxis
.axis
584 self
.graphtexrunner
= self
.linkedto
.graphtexrunner
585 self
.errorname
= "%s (linked to %s)" % (self
.errorname
, linkedaxis
.errorname
)
586 self
.data
= linkedaxis
.data
587 if self
.painter
is _marker
:
588 self
.painter
= linkedaxis
.axis
.linkpainter
591 assert self
.linkedto
is not None, self
.errorname
592 assert self
.positioner
is not None, self
.errorname
593 if self
.canvas
is None:
594 self
.linkedto
.docreate()
595 self
.canvas
= self
.axis
.createlinked(self
.data
, self
.positioner
, self
.graphtexrunner
, self
.errorname
, self
.painter
)
599 class anchoredpathaxis(anchoredaxis
):
600 """an anchored axis along a path"""
602 def __init__(self
, path
, axis
, **kwargs
):
603 anchoredaxis
.__init
__(self
, axis
, text
.defaulttexrunner
, "pathaxis")
604 self
.setpositioner(positioner
.pathpositioner(path
, **kwargs
))
607 def pathaxis(*args
, **kwargs
):
608 """creates an axiscanvas for an axis along a path"""
609 return anchoredpathaxis(*args
, **kwargs
).canvas