PyX now requires Python 2.3 and above
[PyX/mjg.git] / pyx / graph / axis / axis.py
blob7764e7d433dc5e599c87ca88b64853bf8e5cff76
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
24 import math, warnings
25 from pyx import attr, unit, text
26 from pyx.graph.axis import painter, parter, positioner, rater, texter, tick
29 class _marker: pass
32 class axisdata:
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)
44 class _axis:
45 """axis"""
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)
51 return canvas
54 class NoValidPartitionError(RuntimeError):
56 pass
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
70 self.min = min
71 self.max = max
72 self.reverse = reverse
73 self.divisor = divisor
74 self.title = title
75 self.painter = painter
76 self.texter = texter
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)
86 zero = 0.0
88 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
89 if self.min is None or self.max is None:
90 for value in columndata:
91 try:
92 value = value + self.zero
93 except:
94 pass
95 else:
96 if self.min is None and (data.min is None or value < data.min):
97 data.min = value
98 if self.max is None and (data.max is None or value > data.max):
99 data.max = value
101 def checkfraclist(self, fracs):
102 "orders a list of fracs, equal entries are not allowed"
103 if not len(fracs): return []
104 sorted = list(fracs)
105 sorted.sort()
106 last = sorted[0]
107 for item in sorted[1:]:
108 if last == item:
109 raise ValueError("duplicate entry found")
110 last = item
111 return sorted
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:
119 try:
120 data.min, data.max = data.min - 0.5*self.fallbackrange, data.min + 0.5*self.fallbackrange
121 except TypeError:
122 data.min, data.max = self.fallbackrange[0], self.fallbackrange[1]
123 else:
124 raise RuntimeError("zero axis range%s" % errorname)
126 def layout(data):
127 self.adjustaxis(data, data.ticks, graphtexrunner, errorname)
128 self.texter.labels(data.ticks)
129 if self.divisor:
130 for t in 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)
135 return canvas
137 if parter is None:
138 data.ticks = self.manualticks
139 return layout(data)
141 # a variant is a data copy with local modifications to test several partitions
142 class variant:
143 def __init__(self, data, **kwargs):
144 self.data = data
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
156 bestrate = None
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)
160 else:
161 partfunctions = parter.partfunctions(data.min, data.max,
162 self.min is None, self.max is None)
163 variants = []
164 for partfunction in partfunctions:
165 worse = 0
166 while worse < self.maxworse:
167 worse += 1
168 ticks = partfunction()
169 if ticks is None:
170 break
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]
173 if ticks:
174 rate = rater.rateticks(self, ticks, self.density)
175 if self.reverse:
176 rate += rater.raterange(self.convert(data, ticks[0]) -
177 self.convert(data, ticks[-1]), 1)
178 else:
179 rate += rater.raterange(self.convert(data, ticks[-1]) -
180 self.convert(data, ticks[0]), 1)
181 if bestrate is None or rate < bestrate:
182 bestrate = rate
183 worse = 0
184 variants.append(variant(data, rate=rate, ticks=ticks))
186 if not variants:
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
193 return layout(data)
195 # build the layout for best variants
196 for variant in variants:
197 variant.storedcanvas = None
198 variants.sort()
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:
203 del variants[0]
204 if not variants:
205 raise NoValidPartitionError("no valid axis partitioning found%s" % errorname)
206 else:
207 variants[0].rate += ratelayout
208 variants.sort()
209 self.adjustaxis(data, variants[0].ticks, graphtexrunner, errorname)
210 data.ticks = variants[0].ticks
211 return variants[0].storedcanvas
214 class linear(_regularaxis):
215 """linear axis"""
217 def __init__(self, parter=parter.autolinear(), rater=rater.linear(), **args):
218 _regularaxis.__init__(self, **args)
219 self.parter = parter
220 self.rater = rater
222 def convert(self, data, value):
223 """axis coordinates -> graph coordinates"""
224 if self.reverse:
225 return (data.max - float(value)) / (data.max - data.min)
226 else:
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)
232 lin = linear
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)
241 self.parter = parter
242 self.rater = rater
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)
248 if self.reverse:
249 return (math.log(data.max) - math.log(float(value))) / (math.log(data.max) - math.log(data.min))
250 else:
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):
254 try:
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)
260 raise
262 log = logarithmic
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):
276 if v1 is not None:
277 v1 = self.vmin+v1*(self.vmax-self.vmin)
278 else:
279 v1 = self.vminover
280 if v2 is not None:
281 v2 = self.vmin+v2*(self.vmax-self.vmin)
282 else:
283 v2 = self.vmaxover
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))
296 class bar(_axis):
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
303 self.dist = dist
304 if firstdist is not None:
305 self.firstdist = firstdist
306 else:
307 self.firstdist = 0.5 * dist
308 if lastdist is not None:
309 self.lastdist = lastdist
310 else:
311 self.lastdist = 0.5 * dist
312 self.title = title
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=[])
319 return data
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")
325 if subaxis.sized:
326 data.size += subaxis.data.size
327 else:
328 data.size += 1
329 data.size += self.dist
330 data.subaxes[name] = subaxis
331 if self.reverse:
332 data.names.insert(0, name)
333 else:
334 data.names.append(name)
336 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
337 for value in columndata:
339 # some checks and error messages
340 try:
341 len(value)
342 except:
343 raise ValueError("tuple expected by bar axis '%s'" % errorname)
344 try:
345 value + ""
346 except:
347 pass
348 else:
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
352 name = value[0]
353 if name is not None and name not in data.names:
354 if self.subaxes:
355 if self.subaxes[name] is not None:
356 self.addsubaxis(data, name, self.subaxes[name], graphtexrunner, errorname)
357 else:
358 self.addsubaxis(data, name, self.defaultsubaxis, graphtexrunner, errorname)
359 for name in data.names:
360 subaxis = data.subaxes[name]
361 if subaxis.sized:
362 data.size -= subaxis.data.size
363 subaxis.axis.adjustaxis(subaxis.data,
364 [value[1] for value in columndata if value[0] == name],
365 graphtexrunner,
366 "%s, subaxis %s" % (errorname, name))
367 if subaxis.sized:
368 data.size += subaxis.data.size
370 def convert(self, data, value):
371 if value[0] is None:
372 return None
373 axis = data.subaxes[value[0]]
374 vmin = axis.vmin
375 vmax = axis.vmax
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)
380 v = 0
381 position = self.firstdist
382 for name in data.names:
383 subaxis = data.subaxes[name]
384 subaxis.vmin = position / float(data.size)
385 if subaxis.sized:
386 position += subaxis.data.size
387 else:
388 position += 1
389 subaxis.vmax = position / float(data.size)
390 position += 0.5*self.dist
391 subaxis.vminover = v
392 if name == data.names[-1]:
393 subaxis.vmaxover = 1
394 else:
395 subaxis.vmaxover = position / float(data.size)
396 subaxis.setpositioner(subaxispositioner(positioner, subaxis))
397 subaxis.create()
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
402 v = subaxis.vmaxover
403 if self.painter is not None:
404 self.painter.paint(canvas, data, self, positioner)
405 return canvas
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]))
413 subaxis.create()
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)
419 return canvas
422 class nestedbar(bar):
424 def __init__(self, defaultsubaxis=bar(dist=0, painter=None, linkpainter=None), **kwargs):
425 bar.__init__(self, defaultsubaxis=defaultsubaxis, **kwargs)
428 class split(bar):
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)
442 self.size = size
444 def createdata(self, errorname):
445 data = linear.createdata(self, errorname)
446 data.size = self.size
447 return data
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)
459 try:
460 data.size = data.max - data.min
461 except:
462 data.size = 0
463 return data
465 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
466 linear.adjustaxis(self, data, columndata, graphtexrunner, errorname)
467 try:
468 data.size = data.max - data.min
469 except:
470 data.size = 0
472 def create(self, data, positioner, graphtexrunner, errorname):
473 min = data.min
474 max = data.max
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")
478 return canvas
480 autosizedlin = autosizedlinear
483 class anchoredaxis:
485 def __init__(self, axis, graphtexrunner, errorname):
486 assert not isinstance(axis, anchoredaxis), errorname
487 self.axis = axis
488 self.errorname = errorname
489 self.graphtexrunner = graphtexrunner
490 self.data = axis.createdata(self.errorname)
491 self.canvas = None
492 self.positioner = None
494 def setcreatecall(self, function, *args, **kwargs):
495 self._createfunction = function
496 self._createargs = args
497 self._createkwargs = kwargs
499 def docreate(self):
500 if not self.canvas:
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):
509 self.docreate()
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)
515 else:
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):
522 self.docreate()
523 if x1 is None:
524 if x2 is None:
525 return self.positioner.vbasepath()
526 else:
527 return self.positioner.vbasepath(v2=self.axis.convert(self.data, x2))
528 else:
529 if x2 is None:
530 return self.positioner.vbasepath(v1=self.axis.convert(self.data, x1))
531 else:
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):
539 self.docreate()
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):
549 self.docreate()
550 return self.positioner.vtickpoint_pt(self.axis.convert(self.data, x))
552 def tickpoint(self, x):
553 self.docreate()
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):
561 self.docreate()
562 return self.positioner.vtickdirection(self.axis.convert(self.data, x))
564 def create(self):
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)
568 return self.canvas
571 class linkedaxis(anchoredaxis):
573 def __init__(self, linkedaxis=None, errorname="manual-linked", painter=_marker):
574 self.painter = painter
575 self.linkedto = None
576 self.errorname = errorname
577 self.canvas = None
578 self.positioner = None
579 if linkedaxis:
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
592 def create(self):
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)
598 return self.canvas
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))
607 self.create()
609 def pathaxis(*args, **kwargs):
610 """creates an axiscanvas for an axis along a path"""
611 return anchoredpathaxis(*args, **kwargs).canvas