remove shebang -- see comment 3 on https://bugzilla.redhat.com/bugzilla/show_bug...
[PyX/mjg.git] / pyx / graph / axis / axis.py
blobcb2044010cd0258cd20a342498d69fd33c06974d
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
26 import math, warnings
27 from pyx import attr, unit, text
28 from pyx.graph.axis import painter, parter, positioner, rater, texter, tick
30 try:
31 enumerate([])
32 except NameError:
33 # fallback implementation for Python 2.2 and below
34 def enumerate(list):
35 return zip(xrange(len(list)), list)
37 class _marker: pass
40 class axisdata:
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)
52 class _axis:
53 """axis"""
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)
59 return canvas
62 class NoValidPartitionError(RuntimeError):
64 pass
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
78 self.min = min
79 self.max = max
80 self.reverse = reverse
81 self.divisor = divisor
82 self.title = title
83 self.painter = painter
84 self.texter = texter
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)
93 zero = 0.0
95 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
96 if self.min is None or self.max is None:
97 for value in columndata:
98 try:
99 value = value + self.zero
100 except:
101 pass
102 else:
103 if self.min is None and (data.min is None or value < data.min):
104 data.min = value
105 if self.max is None and (data.max is None or value > data.max):
106 data.max = value
108 def checkfraclist(self, fracs):
109 "orders a list of fracs, equal entries are not allowed"
110 if not len(fracs): return []
111 sorted = list(fracs)
112 sorted.sort()
113 last = sorted[0]
114 for item in sorted[1:]:
115 if last == item:
116 raise ValueError("duplicate entry found")
117 last = item
118 return sorted
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)
125 def layout(data):
126 self.adjustaxis(data, data.ticks, graphtexrunner, errorname)
127 self.texter.labels(data.ticks)
128 if self.divisor:
129 for t in 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)
134 return canvas
136 if parter is None:
137 data.ticks = self.manualticks
138 return layout(data)
140 # a variant is a data copy with local modifications to test several partitions
141 class variant:
142 def __init__(self, data, **kwargs):
143 self.data = data
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
155 bestrate = None
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)
159 else:
160 partfunctions = parter.partfunctions(data.min, data.max,
161 self.min is None, self.max is None)
162 variants = []
163 for partfunction in partfunctions:
164 worse = 0
165 while worse < self.maxworse:
166 worse += 1
167 ticks = partfunction()
168 if ticks is None:
169 break
170 ticks = tick.mergeticklists(self.manualticks, ticks, mergeequal=0)
171 if ticks:
172 rate = rater.rateticks(self, ticks, self.density)
173 if self.reverse:
174 rater.raterange(self.convert(data, ticks[0]) -
175 self.convert(data, ticks[-1]), 1)
176 else:
177 rater.raterange(self.convert(data, ticks[-1]) -
178 self.convert(data, ticks[0]), 1)
179 if bestrate is None or rate < bestrate:
180 bestrate = rate
181 worse = 0
182 variants.append(variant(data, rate=rate, ticks=ticks))
184 if not variants:
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
191 return layout(data)
193 # build the layout for best variants
194 for variant in variants:
195 variant.storedcanvas = None
196 variants.sort()
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:
201 del variants[0]
202 if not variants:
203 raise NoValidPartitionError("no valid axis partitioning found%s" % errorname)
204 else:
205 variants[0].rate += ratelayout
206 variants.sort()
207 self.adjustaxis(data, variants[0].ticks, graphtexrunner, errorname)
208 data.ticks = variants[0].ticks
209 return variants[0].storedcanvas
212 class linear(_regularaxis):
213 """linear axis"""
215 def __init__(self, parter=parter.autolinear(), rater=rater.linear(), **args):
216 _regularaxis.__init__(self, **args)
217 self.parter = parter
218 self.rater = rater
220 def convert(self, data, value):
221 """axis coordinates -> graph coordinates"""
222 if self.reverse:
223 return (data.max - float(value)) / (data.max - data.min)
224 else:
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)
230 lin = linear
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)
239 self.parter = parter
240 self.rater = rater
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)
246 if self.reverse:
247 return (math.log(data.max) - math.log(float(value))) / (math.log(data.max) - math.log(data.min))
248 else:
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):
252 try:
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)
258 raise
260 log = logarithmic
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):
274 if v1 is not None:
275 v1 = self.vmin+v1*(self.vmax-self.vmin)
276 else:
277 v1 = self.vminover
278 if v2 is not None:
279 v2 = self.vmin+v2*(self.vmax-self.vmin)
280 else:
281 v2 = self.vmaxover
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))
294 class bar(_axis):
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
301 self.dist = dist
302 if firstdist is not None:
303 self.firstdist = firstdist
304 else:
305 self.firstdist = 0.5 * dist
306 if lastdist is not None:
307 self.lastdist = lastdist
308 else:
309 self.lastdist = 0.5 * dist
310 self.title = title
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=[])
317 return data
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")
323 if subaxis.sized:
324 data.size += subaxis.data.size
325 else:
326 data.size += 1
327 data.size += self.dist
328 data.subaxes[name] = subaxis
329 if self.reverse:
330 data.names.insert(0, name)
331 else:
332 data.names.append(name)
334 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
335 for value in columndata:
337 # some checks and error messages
338 try:
339 len(value)
340 except:
341 raise ValueError("tuple expected by bar axis '%s'" % errorname)
342 try:
343 value + ""
344 except:
345 pass
346 else:
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
350 name = value[0]
351 if name is not None and name not in data.names:
352 if self.subaxes:
353 if self.subaxes[name] is not None:
354 self.addsubaxis(data, name, self.subaxes[name], graphtexrunner, errorname)
355 else:
356 self.addsubaxis(data, name, self.defaultsubaxis, graphtexrunner, errorname)
357 for name in data.names:
358 subaxis = data.subaxes[name]
359 if subaxis.sized:
360 data.size -= subaxis.data.size
361 subaxis.axis.adjustaxis(subaxis.data,
362 [value[1] for value in columndata if value[0] == name],
363 graphtexrunner,
364 "%s, subaxis %s" % (errorname, name))
365 if subaxis.sized:
366 data.size += subaxis.data.size
368 def convert(self, data, value):
369 if value[0] is None:
370 return None
371 axis = data.subaxes[value[0]]
372 vmin = axis.vmin
373 vmax = axis.vmax
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)
378 v = 0
379 position = self.firstdist
380 for name in data.names:
381 subaxis = data.subaxes[name]
382 subaxis.vmin = position / float(data.size)
383 if subaxis.sized:
384 position += subaxis.data.size
385 else:
386 position += 1
387 subaxis.vmax = position / float(data.size)
388 position += 0.5*self.dist
389 subaxis.vminover = v
390 if name == data.names[-1]:
391 subaxis.vmaxover = 1
392 else:
393 subaxis.vmaxover = position / float(data.size)
394 subaxis.setpositioner(subaxispositioner(positioner, subaxis))
395 subaxis.create()
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
400 v = subaxis.vmaxover
401 if self.painter is not None:
402 self.painter.paint(canvas, data, self, positioner)
403 return canvas
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]))
411 subaxis.create()
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)
417 return canvas
420 class nestedbar(bar):
422 def __init__(self, defaultsubaxis=bar(dist=0, painter=None, linkpainter=None), **kwargs):
423 bar.__init__(self, defaultsubaxis=defaultsubaxis, **kwargs)
426 class split(bar):
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)
440 self.size = size
442 def createdata(self, errorname):
443 data = linear.createdata(self, errorname)
444 data.size = self.size
445 return data
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)
457 try:
458 data.size = data.max - data.min
459 except:
460 data.size = 0
461 return data
463 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
464 linear.adjustaxis(self, data, columndata, graphtexrunner, errorname)
465 try:
466 data.size = data.max - data.min
467 except:
468 data.size = 0
470 def create(self, data, positioner, graphtexrunner, errorname):
471 min = data.min
472 max = data.max
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")
476 return canvas
478 autosizedlin = autosizedlinear
481 class anchoredaxis:
483 def __init__(self, axis, graphtexrunner, errorname):
484 assert not isinstance(axis, anchoredaxis), errorname
485 self.axis = axis
486 self.errorname = errorname
487 self.graphtexrunner = graphtexrunner
488 self.data = axis.createdata(errorname)
489 self.canvas = None
490 self.positioner = None
492 def setcreatecall(self, function, *args, **kwargs):
493 self._createfunction = function
494 self._createargs = args
495 self._createkwargs = kwargs
497 def docreate(self):
498 if not self.canvas:
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):
507 self.docreate()
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)
513 else:
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):
520 self.docreate()
521 if x1 is None:
522 if x2 is None:
523 return self.positioner.vbasepath()
524 else:
525 return self.positioner.vbasepath(v2=self.axis.convert(self.data, x2))
526 else:
527 if x2 is None:
528 return self.positioner.vbasepath(v1=self.axis.convert(self.data, x1))
529 else:
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):
537 self.docreate()
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):
547 self.docreate()
548 return self.positioner.vtickpoint_pt(self.axis.convert(self.data, x))
550 def tickpoint(self, x):
551 self.docreate()
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):
559 self.docreate()
560 return self.positioner.vtickdirection(self.axis.convert(self.data, x))
562 def create(self):
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)
566 return self.canvas
569 class linkedaxis(anchoredaxis):
571 def __init__(self, linkedaxis=None, errorname="manual-linked", painter=_marker):
572 self.painter = painter
573 self.linkedto = None
574 self.errorname = errorname
575 self.canvas = None
576 self.positioner = None
577 if linkedaxis:
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
590 def create(self):
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)
596 return self.canvas
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))
605 self.create()
607 def pathaxis(*args, **kwargs):
608 """creates an axiscanvas for an axis along a path"""
609 return anchoredpathaxis(*args, **kwargs).canvas