fix a regression of requiring both, a ticklevel and a labellevel (removing ticks...
[PyX/mjg.git] / pyx / graph / axis / axis.py
blob0ca52c5ef4bcfe7d038d4f97f9135027dff079e6
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 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=[], fallbackrange=None):
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)
89 self.fallbackrange = fallbackrange
91 def createdata(self, errorname):
92 return axisdata(min=self.min, max=self.max)
94 zero = 0.0
96 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
97 if self.min is None or self.max is None:
98 for value in columndata:
99 try:
100 value = value + self.zero
101 except:
102 pass
103 else:
104 if self.min is None and (data.min is None or value < data.min):
105 data.min = value
106 if self.max is None and (data.max is None or value > data.max):
107 data.max = value
109 def checkfraclist(self, fracs):
110 "orders a list of fracs, equal entries are not allowed"
111 if not len(fracs): return []
112 sorted = list(fracs)
113 sorted.sort()
114 last = sorted[0]
115 for item in sorted[1:]:
116 if last == item:
117 raise ValueError("duplicate entry found")
118 last = item
119 return sorted
121 def _create(self, data, positioner, graphtexrunner, parter, rater, errorname):
122 errorname = " for axis %s" % errorname
123 if data.min is None or data.max is None:
124 raise RuntimeError("incomplete axis range%s" % errorname)
125 if data.max == data.min:
126 if self.fallbackrange is not None:
127 try:
128 data.min, data.max = data.min - 0.5*self.fallbackrange, data.min + 0.5*self.fallbackrange
129 except TypeError:
130 data.min, data.max = self.fallbackrange[0], self.fallbackrange[1]
131 else:
132 raise RuntimeError("zero axis range%s" % errorname)
134 def layout(data):
135 self.adjustaxis(data, data.ticks, graphtexrunner, errorname)
136 self.texter.labels(data.ticks)
137 if self.divisor:
138 for t in data.ticks:
139 t *= tick.rational(self.divisor)
140 canvas = painter.axiscanvas(self.painter, graphtexrunner)
141 if self.painter is not None:
142 self.painter.paint(canvas, data, self, positioner)
143 return canvas
145 if parter is None:
146 data.ticks = self.manualticks
147 return layout(data)
149 # a variant is a data copy with local modifications to test several partitions
150 class variant:
151 def __init__(self, data, **kwargs):
152 self.data = data
153 for key, value in kwargs.items():
154 setattr(self, key, value)
156 def __getattr__(self, key):
157 return getattr(data, key)
159 def __cmp__(self, other):
160 # we can also sort variants by their rate
161 return cmp(self.rate, other.rate)
163 # build a list of variants
164 bestrate = None
165 if self.divisor is not None:
166 partfunctions = parter.partfunctions(data.min/self.divisor, data.max/self.divisor,
167 self.min is None, self.max is None)
168 else:
169 partfunctions = parter.partfunctions(data.min, data.max,
170 self.min is None, self.max is None)
171 variants = []
172 for partfunction in partfunctions:
173 worse = 0
174 while worse < self.maxworse:
175 worse += 1
176 ticks = partfunction()
177 if ticks is None:
178 break
179 ticks = [t for t in tick.mergeticklists(self.manualticks, ticks, mergeequal=0)
180 if t.ticklevel is not None or t.labellevel is not None]
181 if ticks:
182 rate = rater.rateticks(self, ticks, self.density)
183 if self.reverse:
184 rate += rater.raterange(self.convert(data, ticks[0]) -
185 self.convert(data, ticks[-1]), 1)
186 else:
187 rate += rater.raterange(self.convert(data, ticks[-1]) -
188 self.convert(data, ticks[0]), 1)
189 if bestrate is None or rate < bestrate:
190 bestrate = rate
191 worse = 0
192 variants.append(variant(data, rate=rate, ticks=ticks))
194 if not variants:
195 raise RuntimeError("no axis partitioning found%s" % errorname)
197 if len(variants) == 1 or self.painter is None:
198 # When the painter is None, we could sort the variants here by their rating.
199 # However, we didn't did this so far and there is no real reason to change that.
200 data.ticks = variants[0].ticks
201 return layout(data)
203 # build the layout for best variants
204 for variant in variants:
205 variant.storedcanvas = None
206 variants.sort()
207 while not variants[0].storedcanvas:
208 variants[0].storedcanvas = layout(variants[0])
209 ratelayout = rater.ratelayout(variants[0].storedcanvas, self.density)
210 if ratelayout is None:
211 del variants[0]
212 if not variants:
213 raise NoValidPartitionError("no valid axis partitioning found%s" % errorname)
214 else:
215 variants[0].rate += ratelayout
216 variants.sort()
217 self.adjustaxis(data, variants[0].ticks, graphtexrunner, errorname)
218 data.ticks = variants[0].ticks
219 return variants[0].storedcanvas
222 class linear(_regularaxis):
223 """linear axis"""
225 def __init__(self, parter=parter.autolinear(), rater=rater.linear(), **args):
226 _regularaxis.__init__(self, **args)
227 self.parter = parter
228 self.rater = rater
230 def convert(self, data, value):
231 """axis coordinates -> graph coordinates"""
232 if self.reverse:
233 return (data.max - float(value)) / (data.max - data.min)
234 else:
235 return (float(value) - data.min) / (data.max - data.min)
237 def create(self, data, positioner, graphtexrunner, errorname):
238 return _regularaxis._create(self, data, positioner, graphtexrunner, self.parter, self.rater, errorname)
240 lin = linear
243 class logarithmic(_regularaxis):
244 """logarithmic axis"""
246 def __init__(self, parter=parter.autologarithmic(), rater=rater.logarithmic(),
247 linearparter=parter.autolinear(extendtick=None), **args):
248 _regularaxis.__init__(self, **args)
249 self.parter = parter
250 self.rater = rater
251 self.linearparter = linearparter
253 def convert(self, data, value):
254 """axis coordinates -> graph coordinates"""
255 # TODO: store log(data.min) and log(data.max)
256 if self.reverse:
257 return (math.log(data.max) - math.log(float(value))) / (math.log(data.max) - math.log(data.min))
258 else:
259 return (math.log(float(value)) - math.log(data.min)) / (math.log(data.max) - math.log(data.min))
261 def create(self, data, positioner, graphtexrunner, errorname):
262 try:
263 return _regularaxis._create(self, data, positioner, graphtexrunner, self.parter, self.rater, errorname)
264 except NoValidPartitionError:
265 if self.linearparter:
266 warnings.warn("no valid logarithmic partitioning found for axis %s, switch to linear partitioning" % errorname)
267 return _regularaxis._create(self, data, positioner, graphtexrunner, self.linearparter, self.rater, errorname)
268 raise
270 log = logarithmic
273 class subaxispositioner(positioner._positioner):
274 """a subaxis positioner"""
276 def __init__(self, basepositioner, subaxis):
277 self.basepositioner = basepositioner
278 self.vmin = subaxis.vmin
279 self.vmax = subaxis.vmax
280 self.vminover = subaxis.vminover
281 self.vmaxover = subaxis.vmaxover
283 def vbasepath(self, v1=None, v2=None):
284 if v1 is not None:
285 v1 = self.vmin+v1*(self.vmax-self.vmin)
286 else:
287 v1 = self.vminover
288 if v2 is not None:
289 v2 = self.vmin+v2*(self.vmax-self.vmin)
290 else:
291 v2 = self.vmaxover
292 return self.basepositioner.vbasepath(v1, v2)
294 def vgridpath(self, v):
295 return self.basepositioner.vgridpath(self.vmin+v*(self.vmax-self.vmin))
297 def vtickpoint_pt(self, v, axis=None):
298 return self.basepositioner.vtickpoint_pt(self.vmin+v*(self.vmax-self.vmin))
300 def vtickdirection(self, v, axis=None):
301 return self.basepositioner.vtickdirection(self.vmin+v*(self.vmax-self.vmin))
304 class bar(_axis):
306 def __init__(self, subaxes=None, defaultsubaxis=linear(painter=None, linkpainter=None, parter=None),
307 dist=0.5, firstdist=None, lastdist=None, title=None, reverse=0,
308 painter=painter.bar(), linkpainter=painter.linkedbar()):
309 self.subaxes = subaxes
310 self.defaultsubaxis = defaultsubaxis
311 self.dist = dist
312 if firstdist is not None:
313 self.firstdist = firstdist
314 else:
315 self.firstdist = 0.5 * dist
316 if lastdist is not None:
317 self.lastdist = lastdist
318 else:
319 self.lastdist = 0.5 * dist
320 self.title = title
321 self.reverse = reverse
322 self.painter = painter
323 self.linkpainter = linkpainter
325 def createdata(self, errorname):
326 data = axisdata(size=self.firstdist+self.lastdist-self.dist, subaxes={}, names=[])
327 return data
329 def addsubaxis(self, data, name, subaxis, graphtexrunner, errorname):
330 subaxis = anchoredaxis(subaxis, graphtexrunner, "%s, subaxis %s" % (errorname, name))
331 subaxis.setcreatecall(lambda: None)
332 subaxis.sized = hasattr(subaxis.data, "size")
333 if subaxis.sized:
334 data.size += subaxis.data.size
335 else:
336 data.size += 1
337 data.size += self.dist
338 data.subaxes[name] = subaxis
339 if self.reverse:
340 data.names.insert(0, name)
341 else:
342 data.names.append(name)
344 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
345 for value in columndata:
347 # some checks and error messages
348 try:
349 len(value)
350 except:
351 raise ValueError("tuple expected by bar axis '%s'" % errorname)
352 try:
353 value + ""
354 except:
355 pass
356 else:
357 raise ValueError("tuple expected by bar axis '%s'" % errorname)
358 assert len(value) == 2, "tuple of size two expected by bar axis '%s'" % errorname
360 name = value[0]
361 if name is not None and name not in data.names:
362 if self.subaxes:
363 if self.subaxes[name] is not None:
364 self.addsubaxis(data, name, self.subaxes[name], graphtexrunner, errorname)
365 else:
366 self.addsubaxis(data, name, self.defaultsubaxis, graphtexrunner, errorname)
367 for name in data.names:
368 subaxis = data.subaxes[name]
369 if subaxis.sized:
370 data.size -= subaxis.data.size
371 subaxis.axis.adjustaxis(subaxis.data,
372 [value[1] for value in columndata if value[0] == name],
373 graphtexrunner,
374 "%s, subaxis %s" % (errorname, name))
375 if subaxis.sized:
376 data.size += subaxis.data.size
378 def convert(self, data, value):
379 if value[0] is None:
380 return None
381 axis = data.subaxes[value[0]]
382 vmin = axis.vmin
383 vmax = axis.vmax
384 return axis.vmin + axis.convert(value[1]) * (axis.vmax - axis.vmin)
386 def create(self, data, positioner, graphtexrunner, errorname):
387 canvas = painter.axiscanvas(self.painter, graphtexrunner)
388 v = 0
389 position = self.firstdist
390 for name in data.names:
391 subaxis = data.subaxes[name]
392 subaxis.vmin = position / float(data.size)
393 if subaxis.sized:
394 position += subaxis.data.size
395 else:
396 position += 1
397 subaxis.vmax = position / float(data.size)
398 position += 0.5*self.dist
399 subaxis.vminover = v
400 if name == data.names[-1]:
401 subaxis.vmaxover = 1
402 else:
403 subaxis.vmaxover = position / float(data.size)
404 subaxis.setpositioner(subaxispositioner(positioner, subaxis))
405 subaxis.create()
406 canvas.insert(subaxis.canvas)
407 if canvas.extent_pt < subaxis.canvas.extent_pt:
408 canvas.extent_pt = subaxis.canvas.extent_pt
409 position += 0.5*self.dist
410 v = subaxis.vmaxover
411 if self.painter is not None:
412 self.painter.paint(canvas, data, self, positioner)
413 return canvas
415 def createlinked(self, data, positioner, graphtexrunner, errorname, linkpainter):
416 canvas = painter.axiscanvas(self.painter, graphtexrunner)
417 for name in data.names:
418 subaxis = data.subaxes[name]
419 subaxis = linkedaxis(subaxis, name)
420 subaxis.setpositioner(subaxispositioner(positioner, data.subaxes[name]))
421 subaxis.create()
422 canvas.insert(subaxis.canvas)
423 if canvas.extent_pt < subaxis.canvas.extent_pt:
424 canvas.extent_pt = subaxis.canvas.extent_pt
425 if linkpainter is not None:
426 linkpainter.paint(canvas, data, self, positioner)
427 return canvas
430 class nestedbar(bar):
432 def __init__(self, defaultsubaxis=bar(dist=0, painter=None, linkpainter=None), **kwargs):
433 bar.__init__(self, defaultsubaxis=defaultsubaxis, **kwargs)
436 class split(bar):
438 def __init__(self, defaultsubaxis=linear(),
439 firstdist=0, lastdist=0,
440 painter=painter.split(), linkpainter=painter.linkedsplit(), **kwargs):
441 bar.__init__(self, defaultsubaxis=defaultsubaxis,
442 firstdist=firstdist, lastdist=lastdist,
443 painter=painter, linkpainter=linkpainter, **kwargs)
446 class sizedlinear(linear):
448 def __init__(self, size=1, **kwargs):
449 linear.__init__(self, **kwargs)
450 self.size = size
452 def createdata(self, errorname):
453 data = linear.createdata(self, errorname)
454 data.size = self.size
455 return data
457 sizedlin = sizedlinear
460 class autosizedlinear(linear):
462 def __init__(self, parter=parter.autolinear(extendtick=None), **kwargs):
463 linear.__init__(self, parter=parter, **kwargs)
465 def createdata(self, errorname):
466 data = linear.createdata(self, errorname)
467 try:
468 data.size = data.max - data.min
469 except:
470 data.size = 0
471 return data
473 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
474 linear.adjustaxis(self, data, columndata, graphtexrunner, errorname)
475 try:
476 data.size = data.max - data.min
477 except:
478 data.size = 0
480 def create(self, data, positioner, graphtexrunner, errorname):
481 min = data.min
482 max = data.max
483 canvas = linear.create(self, data, positioner, graphtexrunner, errorname)
484 if min != data.min or max != data.max:
485 raise RuntimeError("range change during axis creation of autosized linear axis")
486 return canvas
488 autosizedlin = autosizedlinear
491 class anchoredaxis:
493 def __init__(self, axis, graphtexrunner, errorname):
494 assert not isinstance(axis, anchoredaxis), errorname
495 self.axis = axis
496 self.errorname = errorname
497 self.graphtexrunner = graphtexrunner
498 self.data = axis.createdata(self.errorname)
499 self.canvas = None
500 self.positioner = None
502 def setcreatecall(self, function, *args, **kwargs):
503 self._createfunction = function
504 self._createargs = args
505 self._createkwargs = kwargs
507 def docreate(self):
508 if not self.canvas:
509 self._createfunction(*self._createargs, **self._createkwargs)
511 def setpositioner(self, positioner):
512 assert positioner is not None, self.errorname
513 assert self.positioner is None, self.errorname
514 self.positioner = positioner
516 def convert(self, x):
517 self.docreate()
518 return self.axis.convert(self.data, x)
520 def adjustaxis(self, columndata):
521 if self.canvas is None:
522 self.axis.adjustaxis(self.data, columndata, self.graphtexrunner, self.errorname)
523 else:
524 warnings.warn("ignore axis range adjustment of already created axis '%s'" % self.errorname)
526 def vbasepath(self, v1=None, v2=None):
527 return self.positioner.vbasepath(v1=v1, v2=v2)
529 def basepath(self, x1=None, x2=None):
530 self.docreate()
531 if x1 is None:
532 if x2 is None:
533 return self.positioner.vbasepath()
534 else:
535 return self.positioner.vbasepath(v2=self.axis.convert(self.data, x2))
536 else:
537 if x2 is None:
538 return self.positioner.vbasepath(v1=self.axis.convert(self.data, x1))
539 else:
540 return self.positioner.vbasepath(v1=self.axis.convert(self.data, x1),
541 v2=self.axis.convert(self.data, x2))
543 def vgridpath(self, v):
544 return self.positioner.vgridpath(v)
546 def gridpath(self, x):
547 self.docreate()
548 return self.positioner.vgridpath(self.axis.convert(self.data, x))
550 def vtickpoint_pt(self, v):
551 return self.positioner.vtickpoint_pt(v)
553 def vtickpoint(self, v):
554 return self.positioner.vtickpoint_pt(v) * unit.t_pt
556 def tickpoint_pt(self, x):
557 self.docreate()
558 return self.positioner.vtickpoint_pt(self.axis.convert(self.data, x))
560 def tickpoint(self, x):
561 self.docreate()
562 x_pt, y_pt = self.positioner.vtickpoint_pt(self.axis.convert(self.data, x))
563 return x_pt * unit.t_pt, y_pt * unit.t_pt
565 def vtickdirection(self, v):
566 return self.positioner.vtickdirection(v)
568 def tickdirection(self, x):
569 self.docreate()
570 return self.positioner.vtickdirection(self.axis.convert(self.data, x))
572 def create(self):
573 if self.canvas is None:
574 assert self.positioner is not None, self.errorname
575 self.canvas = self.axis.create(self.data, self.positioner, self.graphtexrunner, self.errorname)
576 return self.canvas
579 class linkedaxis(anchoredaxis):
581 def __init__(self, linkedaxis=None, errorname="manual-linked", painter=_marker):
582 self.painter = painter
583 self.linkedto = None
584 self.errorname = errorname
585 self.canvas = None
586 self.positioner = None
587 if linkedaxis:
588 self.setlinkedaxis(linkedaxis)
590 def setlinkedaxis(self, linkedaxis):
591 assert isinstance(linkedaxis, anchoredaxis), self.errorname
592 self.linkedto = linkedaxis
593 self.axis = linkedaxis.axis
594 self.graphtexrunner = self.linkedto.graphtexrunner
595 self.errorname = "%s (linked to %s)" % (self.errorname, linkedaxis.errorname)
596 self.data = linkedaxis.data
597 if self.painter is _marker:
598 self.painter = linkedaxis.axis.linkpainter
600 def create(self):
601 assert self.linkedto is not None, self.errorname
602 assert self.positioner is not None, self.errorname
603 if self.canvas is None:
604 self.linkedto.docreate()
605 self.canvas = self.axis.createlinked(self.data, self.positioner, self.graphtexrunner, self.errorname, self.painter)
606 return self.canvas
609 class anchoredpathaxis(anchoredaxis):
610 """an anchored axis along a path"""
612 def __init__(self, path, axis, **kwargs):
613 anchoredaxis.__init__(self, axis, text.defaulttexrunner, "pathaxis")
614 self.setpositioner(positioner.pathpositioner(path, **kwargs))
615 self.create()
617 def pathaxis(*args, **kwargs):
618 """creates an axiscanvas for an axis along a path"""
619 return anchoredpathaxis(*args, **kwargs).canvas