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
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
)
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
150 def __init__(self
, data
, **kwargs
):
152 for key
, value
in kwargs
.items():
153 setattr(self
, key
, value
)
155 def __getattr__(self
, key
):
156 return getattr(data
, key
)
158 def __cmp__(self
, other
):
159 # we can also sort variants by their rate
160 return cmp(self
.rate
, other
.rate
)
162 # build a list of variants
164 if self
.divisor
is not None:
165 if data
.min is not None:
166 data_min_divided
= data
.min/self
.divisor
168 data_min_divided
= None
169 if data
.max is not None:
170 data_max_divided
= data
.max/self
.divisor
172 data_max_divided
= None
173 partfunctions
= parter
.partfunctions(data_min_divided
, data_max_divided
,
174 self
.min is None, self
.max is None)
176 partfunctions
= parter
.partfunctions(data
.min, data
.max,
177 self
.min is None, self
.max is None)
179 for partfunction
in partfunctions
:
181 while worse
< self
.maxworse
:
183 ticks
= partfunction()
186 ticks
= tick
.mergeticklists(self
.manualticks
, ticks
, mergeequal
=0)
188 rate
= rater
.rateticks(self
, ticks
, self
.density
)
191 rate
+= rater
.raterange(self
.convert(data
, convert_tick(ticks
[0])) -
192 self
.convert(data
, convert_tick(ticks
[-1])), 1)
194 rate
+= rater
.raterange(self
.convert(data
, convert_tick(ticks
[-1])) -
195 self
.convert(data
, convert_tick(ticks
[0])), 1)
196 if bestrate
is None or rate
< bestrate
:
199 variants
.append(variant(data
, rate
=rate
, ticks
=ticks
))
202 raise RuntimeError("no axis partitioning found%s" % errorname
)
204 if len(variants
) == 1 or self
.painter
is None:
205 # When the painter is None, we could sort the variants here by their rating.
206 # However, we didn't did this so far and there is no real reason to change that.
207 data
.ticks
= variants
[0].ticks
210 # build the layout for best variants
211 for variant
in variants
:
212 variant
.storedcanvas
= None
214 while not variants
[0].storedcanvas
:
215 variants
[0].storedcanvas
= layout(variants
[0])
216 ratelayout
= rater
.ratelayout(variants
[0].storedcanvas
, self
.density
)
217 if ratelayout
is None:
220 raise NoValidPartitionError("no valid axis partitioning found%s" % errorname
)
222 variants
[0].rate
+= ratelayout
224 self
.adjustaxis(data
, variants
[0].ticks
, graphtexrunner
, errorname
)
225 data
.ticks
= variants
[0].ticks
226 return variants
[0].storedcanvas
229 class linear(_regularaxis
):
232 def __init__(self
, parter
=parter
.autolinear(), rater
=rater
.linear(), **args
):
233 _regularaxis
.__init
__(self
, **args
)
237 def convert(self
, data
, value
):
238 """axis coordinates -> graph coordinates"""
240 return (data
.max - float(value
)) / (data
.max - data
.min)
242 return (float(value
) - data
.min) / (data
.max - data
.min)
244 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
245 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.parter
, self
.rater
, errorname
)
250 class logarithmic(_regularaxis
):
251 """logarithmic axis"""
253 def __init__(self
, parter
=parter
.autologarithmic(), rater
=rater
.logarithmic(),
254 linearparter
=parter
.autolinear(extendtick
=None), **args
):
255 _regularaxis
.__init
__(self
, **args
)
258 self
.linearparter
= linearparter
260 def convert(self
, data
, value
):
261 """axis coordinates -> graph coordinates"""
262 # TODO: store log(data.min) and log(data.max)
264 return (math
.log(data
.max) - math
.log(float(value
))) / (math
.log(data
.max) - math
.log(data
.min))
266 return (math
.log(float(value
)) - math
.log(data
.min)) / (math
.log(data
.max) - math
.log(data
.min))
268 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
270 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.parter
, self
.rater
, errorname
)
271 except NoValidPartitionError
:
272 if self
.linearparter
:
273 warnings
.warn("no valid logarithmic partitioning found for axis %s, switch to linear partitioning" % errorname
)
274 return _regularaxis
._create
(self
, data
, positioner
, graphtexrunner
, self
.linearparter
, self
.rater
, errorname
)
280 class subaxispositioner(positioner
._positioner
):
281 """a subaxis positioner"""
283 def __init__(self
, basepositioner
, subaxis
):
284 self
.basepositioner
= basepositioner
285 self
.vmin
= subaxis
.vmin
286 self
.vmax
= subaxis
.vmax
287 self
.vminover
= subaxis
.vminover
288 self
.vmaxover
= subaxis
.vmaxover
290 def vbasepath(self
, v1
=None, v2
=None):
292 v1
= self
.vmin
+v1
*(self
.vmax
-self
.vmin
)
296 v2
= self
.vmin
+v2
*(self
.vmax
-self
.vmin
)
299 return self
.basepositioner
.vbasepath(v1
, v2
)
301 def vgridpath(self
, v
):
302 return self
.basepositioner
.vgridpath(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
304 def vtickpoint_pt(self
, v
, axis
=None):
305 return self
.basepositioner
.vtickpoint_pt(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
307 def vtickdirection(self
, v
, axis
=None):
308 return self
.basepositioner
.vtickdirection(self
.vmin
+v
*(self
.vmax
-self
.vmin
))
313 def __init__(self
, subaxes
=None, defaultsubaxis
=linear(painter
=None, linkpainter
=None, parter
=None),
314 dist
=0.5, firstdist
=None, lastdist
=None, title
=None, reverse
=0,
315 painter
=painter
.bar(), linkpainter
=painter
.linkedbar()):
316 self
.subaxes
= subaxes
317 self
.defaultsubaxis
= defaultsubaxis
319 if firstdist
is not None:
320 self
.firstdist
= firstdist
322 self
.firstdist
= 0.5 * dist
323 if lastdist
is not None:
324 self
.lastdist
= lastdist
326 self
.lastdist
= 0.5 * dist
328 self
.reverse
= reverse
329 self
.painter
= painter
330 self
.linkpainter
= linkpainter
332 def createdata(self
, errorname
):
333 data
= axisdata(size
=self
.firstdist
+self
.lastdist
-self
.dist
, subaxes
={}, names
=[])
336 def addsubaxis(self
, data
, name
, subaxis
, graphtexrunner
, errorname
):
337 subaxis
= anchoredaxis(subaxis
, graphtexrunner
, "%s, subaxis %s" % (errorname
, name
))
338 subaxis
.setcreatecall(lambda: None)
339 subaxis
.sized
= hasattr(subaxis
.data
, "size")
341 data
.size
+= subaxis
.data
.size
344 data
.size
+= self
.dist
345 data
.subaxes
[name
] = subaxis
347 data
.names
.insert(0, name
)
349 data
.names
.append(name
)
351 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
352 for value
in columndata
:
354 # some checks and error messages
358 raise ValueError("tuple expected by bar axis '%s'" % errorname
)
364 raise ValueError("tuple expected by bar axis '%s'" % errorname
)
365 assert len(value
) == 2, "tuple of size two expected by bar axis '%s'" % errorname
368 if name
is not None and name
not in data
.names
:
370 if self
.subaxes
[name
] is not None:
371 self
.addsubaxis(data
, name
, self
.subaxes
[name
], graphtexrunner
, errorname
)
373 self
.addsubaxis(data
, name
, self
.defaultsubaxis
, graphtexrunner
, errorname
)
374 for name
in data
.names
:
375 subaxis
= data
.subaxes
[name
]
377 data
.size
-= subaxis
.data
.size
378 subaxis
.axis
.adjustaxis(subaxis
.data
,
379 [value
[1] for value
in columndata
if value
[0] == name
],
381 "%s, subaxis %s" % (errorname
, name
))
383 data
.size
+= subaxis
.data
.size
385 def convert(self
, data
, value
):
388 axis
= data
.subaxes
[value
[0]]
391 return axis
.vmin
+ axis
.convert(value
[1]) * (axis
.vmax
- axis
.vmin
)
393 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
394 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
396 position
= self
.firstdist
397 for name
in data
.names
:
398 subaxis
= data
.subaxes
[name
]
399 subaxis
.vmin
= position
/ float(data
.size
)
401 position
+= subaxis
.data
.size
404 subaxis
.vmax
= position
/ float(data
.size
)
405 position
+= 0.5*self
.dist
407 if name
== data
.names
[-1]:
410 subaxis
.vmaxover
= position
/ float(data
.size
)
411 subaxis
.setpositioner(subaxispositioner(positioner
, subaxis
))
413 for layer
, subcanvas
in subaxis
.canvas
.layers
.items():
414 canvas
.layer(layer
).insert(subcanvas
)
415 assert len(subaxis
.canvas
.layers
) == len(subaxis
.canvas
.items
)
416 if canvas
.extent_pt
< subaxis
.canvas
.extent_pt
:
417 canvas
.extent_pt
= subaxis
.canvas
.extent_pt
418 position
+= 0.5*self
.dist
420 if self
.painter
is not None:
421 self
.painter
.paint(canvas
, data
, self
, positioner
)
424 def createlinked(self
, data
, positioner
, graphtexrunner
, errorname
, linkpainter
):
425 canvas
= painter
.axiscanvas(self
.painter
, graphtexrunner
)
426 for name
in data
.names
:
427 subaxis
= data
.subaxes
[name
]
428 subaxis
= linkedaxis(subaxis
, name
)
429 subaxis
.setpositioner(subaxispositioner(positioner
, data
.subaxes
[name
]))
431 for layer
, subcanvas
in subaxis
.canvas
.layers
.items():
432 canvas
.layer(layer
).insert(subcanvas
)
433 assert len(subaxis
.canvas
.layers
) == len(subaxis
.canvas
.items
)
434 if canvas
.extent_pt
< subaxis
.canvas
.extent_pt
:
435 canvas
.extent_pt
= subaxis
.canvas
.extent_pt
436 if linkpainter
is not None:
437 linkpainter
.paint(canvas
, data
, self
, positioner
)
441 class nestedbar(bar
):
443 def __init__(self
, defaultsubaxis
=bar(dist
=0, painter
=None, linkpainter
=None), **kwargs
):
444 bar
.__init
__(self
, defaultsubaxis
=defaultsubaxis
, **kwargs
)
449 def __init__(self
, defaultsubaxis
=linear(),
450 firstdist
=0, lastdist
=0,
451 painter
=painter
.split(), linkpainter
=painter
.linkedsplit(), **kwargs
):
452 bar
.__init
__(self
, defaultsubaxis
=defaultsubaxis
,
453 firstdist
=firstdist
, lastdist
=lastdist
,
454 painter
=painter
, linkpainter
=linkpainter
, **kwargs
)
457 class sizedlinear(linear
):
459 def __init__(self
, size
=1, **kwargs
):
460 linear
.__init
__(self
, **kwargs
)
463 def createdata(self
, errorname
):
464 data
= linear
.createdata(self
, errorname
)
465 data
.size
= self
.size
468 sizedlin
= sizedlinear
471 class autosizedlinear(linear
):
473 def __init__(self
, parter
=parter
.autolinear(extendtick
=None), **kwargs
):
474 linear
.__init
__(self
, parter
=parter
, **kwargs
)
476 def createdata(self
, errorname
):
477 data
= linear
.createdata(self
, errorname
)
479 data
.size
= data
.max - data
.min
484 def adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
):
485 linear
.adjustaxis(self
, data
, columndata
, graphtexrunner
, errorname
)
487 data
.size
= data
.max - data
.min
491 def create(self
, data
, positioner
, graphtexrunner
, errorname
):
494 canvas
= linear
.create(self
, data
, positioner
, graphtexrunner
, errorname
)
495 if min != data
.min or max != data
.max:
496 raise RuntimeError("range change during axis creation of autosized linear axis")
499 autosizedlin
= autosizedlinear
504 def __init__(self
, axis
, graphtexrunner
, errorname
):
505 assert not isinstance(axis
, anchoredaxis
), errorname
507 self
.errorname
= errorname
508 self
.graphtexrunner
= graphtexrunner
509 self
.data
= axis
.createdata(self
.errorname
)
511 self
.positioner
= None
513 def setcreatecall(self
, function
, *args
, **kwargs
):
514 self
._createfunction
= function
515 self
._createargs
= args
516 self
._createkwargs
= kwargs
520 self
._createfunction
(*self
._createargs
, **self
._createkwargs
)
522 def setpositioner(self
, positioner
):
523 assert positioner
is not None, self
.errorname
524 assert self
.positioner
is None, self
.errorname
525 self
.positioner
= positioner
527 def convert(self
, x
):
529 return self
.axis
.convert(self
.data
, x
)
531 def adjustaxis(self
, columndata
):
532 if self
.canvas
is None:
533 self
.axis
.adjustaxis(self
.data
, columndata
, self
.graphtexrunner
, self
.errorname
)
535 warnings
.warn("ignore axis range adjustment of already created axis '%s'" % self
.errorname
)
537 def vbasepath(self
, v1
=None, v2
=None):
538 return self
.positioner
.vbasepath(v1
=v1
, v2
=v2
)
540 def basepath(self
, x1
=None, x2
=None):
544 return self
.positioner
.vbasepath()
546 return self
.positioner
.vbasepath(v2
=self
.axis
.convert(self
.data
, x2
))
549 return self
.positioner
.vbasepath(v1
=self
.axis
.convert(self
.data
, x1
))
551 return self
.positioner
.vbasepath(v1
=self
.axis
.convert(self
.data
, x1
),
552 v2
=self
.axis
.convert(self
.data
, x2
))
554 def vgridpath(self
, v
):
555 return self
.positioner
.vgridpath(v
)
557 def gridpath(self
, x
):
559 return self
.positioner
.vgridpath(self
.axis
.convert(self
.data
, x
))
561 def vtickpoint_pt(self
, v
):
562 return self
.positioner
.vtickpoint_pt(v
)
564 def vtickpoint(self
, v
):
565 return self
.positioner
.vtickpoint_pt(v
) * unit
.t_pt
567 def tickpoint_pt(self
, x
):
569 return self
.positioner
.vtickpoint_pt(self
.axis
.convert(self
.data
, x
))
571 def tickpoint(self
, x
):
573 x_pt
, y_pt
= self
.positioner
.vtickpoint_pt(self
.axis
.convert(self
.data
, x
))
574 return x_pt
* unit
.t_pt
, y_pt
* unit
.t_pt
576 def vtickdirection(self
, v
):
577 return self
.positioner
.vtickdirection(v
)
579 def tickdirection(self
, x
):
581 return self
.positioner
.vtickdirection(self
.axis
.convert(self
.data
, x
))
584 if self
.canvas
is None:
585 assert self
.positioner
is not None, self
.errorname
586 self
.canvas
= self
.axis
.create(self
.data
, self
.positioner
, self
.graphtexrunner
, self
.errorname
)
590 class linkedaxis(anchoredaxis
):
592 def __init__(self
, linkedaxis
=None, errorname
="manual-linked", painter
=_marker
):
593 self
.painter
= painter
595 self
.errorname
= errorname
597 self
.positioner
= None
599 self
.setlinkedaxis(linkedaxis
)
601 def setlinkedaxis(self
, linkedaxis
):
602 assert isinstance(linkedaxis
, anchoredaxis
), self
.errorname
603 self
.linkedto
= linkedaxis
604 self
.axis
= linkedaxis
.axis
605 self
.graphtexrunner
= self
.linkedto
.graphtexrunner
606 self
.errorname
= "%s (linked to %s)" % (self
.errorname
, linkedaxis
.errorname
)
607 self
.data
= linkedaxis
.data
608 if self
.painter
is _marker
:
609 self
.painter
= linkedaxis
.axis
.linkpainter
612 assert self
.linkedto
is not None, self
.errorname
613 assert self
.positioner
is not None, self
.errorname
614 if self
.canvas
is None:
615 self
.linkedto
.docreate()
616 self
.canvas
= self
.axis
.createlinked(self
.data
, self
.positioner
, self
.graphtexrunner
, self
.errorname
, self
.painter
)
620 class anchoredpathaxis(anchoredaxis
):
621 """an anchored axis along a path"""
623 def __init__(self
, path
, axis
, **kwargs
):
624 anchoredaxis
.__init
__(self
, axis
, text
.defaulttexrunner
, "pathaxis")
625 self
.setpositioner(positioner
.pathpositioner(path
, **kwargs
))
628 def pathaxis(*args
, **kwargs
):
629 """creates an axiscanvas for an axis along a path"""
630 return anchoredpathaxis(*args
, **kwargs
).canvas