fallback (with warning) to linear partitioner on a small logarithmics scale
[PyX.git] / pyx / graph / axis / axis.py
blob4c0014aa3b34383ee2744e56f24f107f7fb031b0
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2005 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 from __future__ import nested_scopes
27 import math, warnings
28 from pyx import attr, unit, text
29 from pyx.graph.axis import painter, parter, positioner, rater, texter, tick
31 try:
32 enumerate([])
33 except NameError:
34 # fallback implementation for Python 2.2 and below
35 def enumerate(list):
36 return zip(xrange(len(list)), list)
38 class _marker: pass
41 class axisdata:
42 """axis data storage class
44 Instances of this class are used to store axis data local to the
45 graph. It will always contain an axispos instance provided by the
46 graph during initialization."""
48 def __init__(self, **kwargs):
49 for key, value in kwargs.items():
50 setattr(self, key, value)
53 class _axis:
54 """axis"""
56 def createlinked(self, data, positioner, graphtexrunner, errorname, linkpainter):
57 canvas = painter.axiscanvas(self.painter, graphtexrunner)
58 if linkpainter is not None:
59 linkpainter.paint(canvas, data, self, positioner)
60 return canvas
63 class NoValidPartitionError(RuntimeError):
65 pass
68 class _regularaxis(_axis):
69 """base implementation a regular axis
71 Regular axis have a continuous variable like linear axes,
72 logarithmic axes, time axes etc."""
74 def __init__(self, min=None, max=None, reverse=0, divisor=None, title=None,
75 painter=painter.regular(), texter=texter.mixed(), linkpainter=painter.linked(),
76 density=1, maxworse=2, manualticks=[]):
77 if min is not None and max is not None and min > max:
78 min, max, reverse = max, min, not reverse
79 self.min = min
80 self.max = max
81 self.reverse = reverse
82 self.divisor = divisor
83 self.title = title
84 self.painter = painter
85 self.texter = texter
86 self.linkpainter = linkpainter
87 self.density = density
88 self.maxworse = maxworse
89 self.manualticks = self.checkfraclist(manualticks)
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)
126 # a variant is a data copy with local modifications to test several partitions
127 class variant:
128 def __init__(self, data, **kwargs):
129 self.data = data
130 for key, value in kwargs.items():
131 setattr(self, key, value)
133 def __getattr__(self, key):
134 return getattr(data, key)
136 def __cmp__(self, other):
137 return cmp(self.rate, other.rate)
139 # build a list of variants
140 bestrate = None
141 if parter is not None:
142 if self.divisor is not None:
143 partfunctions = parter.partfunctions(data.min/self.divisor, data.max/self.divisor,
144 self.min is None, self.max is None)
145 else:
146 partfunctions = parter.partfunctions(data.min, data.max,
147 self.min is None, self.max is None)
148 variants = []
149 for partfunction in partfunctions:
150 worse = 0
151 while worse < self.maxworse:
152 worse += 1
153 ticks = partfunction()
154 if ticks is None:
155 break
156 ticks = tick.mergeticklists(self.manualticks, ticks, mergeequal=0)
157 if ticks:
158 rate = rater.rateticks(self, ticks, self.density)
159 if self.reverse:
160 rater.raterange(self.convert(data, ticks[0]) -
161 self.convert(data, ticks[-1]), 1)
162 else:
163 rater.raterange(self.convert(data, ticks[-1]) -
164 self.convert(data, ticks[0]), 1)
165 if bestrate is None or rate < bestrate:
166 bestrate = rate
167 worse = 0
168 variants.append(variant(data, rate=rate, ticks=ticks))
169 if not variants:
170 raise RuntimeError("no axis partitioning found%s" % errorname)
171 else:
172 variants = [variant(data, rate=0, ticks=self.manualticks)]
174 # get best variant
175 if self.painter is None or len(variants) == 1:
176 # in case of a single variant we're almost done
177 self.adjustaxis(data, variants[0].ticks, graphtexrunner, errorname)
178 if variants[0].ticks:
179 self.texter.labels(variants[0].ticks)
180 if self.divisor:
181 for t in variants[0].ticks:
182 t *= tick.rational(self.divisor)
183 canvas = painter.axiscanvas(self.painter, graphtexrunner)
184 data.ticks = variants[0].ticks
185 if self.painter is not None:
186 self.painter.paint(canvas, data, self, positioner)
187 return canvas
188 else:
189 # build the layout for best variants
190 for variant in variants:
191 variant.storedcanvas = None
192 variants.sort()
193 while not variants[0].storedcanvas:
194 self.adjustaxis(variants[0], variants[0].ticks, graphtexrunner, errorname)
195 if variants[0].ticks:
196 self.texter.labels(variants[0].ticks)
197 if self.divisor:
198 for t in variants[0].ticks:
199 t *= tick.rational(self.divisor)
200 canvas = painter.axiscanvas(self.painter, graphtexrunner)
201 self.painter.paint(canvas, variants[0], self, positioner)
202 ratelayout = rater.ratelayout(canvas, self.density)
203 if ratelayout is None:
204 del variants[0]
205 if not variants:
206 raise NoValidPartitionError("no valid axis partitioning found%s" % errorname)
207 else:
208 variants[0].rate += ratelayout
209 variants[0].storedcanvas = canvas
210 variants.sort()
211 self.adjustaxis(data, variants[0].ticks, graphtexrunner, errorname)
212 data.ticks = variants[0].ticks
213 return variants[0].storedcanvas
216 class linear(_regularaxis):
217 """linear axis"""
219 def __init__(self, parter=parter.autolinear(), rater=rater.linear(), **args):
220 _regularaxis.__init__(self, **args)
221 self.parter = parter
222 self.rater = rater
224 def convert(self, data, value):
225 """axis coordinates -> graph coordinates"""
226 if self.reverse:
227 return (data.max - float(value)) / (data.max - data.min)
228 else:
229 return (float(value) - data.min) / (data.max - data.min)
231 def create(self, data, positioner, graphtexrunner, errorname):
232 return _regularaxis._create(self, data, positioner, graphtexrunner, self.parter, self.rater, errorname)
234 lin = linear
237 class logarithmic(_regularaxis):
238 """logarithmic axis"""
240 def __init__(self, parter=parter.autologarithmic(), rater=rater.logarithmic(),
241 linearparter=parter.autolinear(extendtick=None), **args):
242 _regularaxis.__init__(self, **args)
243 self.parter = parter
244 self.rater = rater
245 self.linearparter = linearparter
247 def convert(self, data, value):
248 """axis coordinates -> graph coordinates"""
249 # TODO: store log(data.min) and log(data.max)
250 if self.reverse:
251 return (math.log(data.max) - math.log(float(value))) / (math.log(data.max) - math.log(data.min))
252 else:
253 return (math.log(float(value)) - math.log(data.min)) / (math.log(data.max) - math.log(data.min))
255 def create(self, data, positioner, graphtexrunner, errorname):
256 try:
257 return _regularaxis._create(self, data, positioner, graphtexrunner, self.parter, self.rater, errorname)
258 except NoValidPartitionError:
259 if self.linearparter:
260 warnings.warn("no valid logarithmic partitioning found for axis %s, switch to linear partitioning" % errorname)
261 return _regularaxis._create(self, data, positioner, graphtexrunner, self.linearparter, self.rater, errorname)
262 raise
264 log = logarithmic
267 class subaxispositioner(positioner._positioner):
268 """a subaxis positioner"""
270 def __init__(self, basepositioner, subaxis):
271 self.basepositioner = basepositioner
272 self.vmin = subaxis.vmin
273 self.vmax = subaxis.vmax
274 self.vminover = subaxis.vminover
275 self.vmaxover = subaxis.vmaxover
277 def vbasepath(self, v1=None, v2=None):
278 if v1 is not None:
279 v1 = self.vmin+v1*(self.vmax-self.vmin)
280 else:
281 v1 = self.vminover
282 if v2 is not None:
283 v2 = self.vmin+v2*(self.vmax-self.vmin)
284 else:
285 v2 = self.vmaxover
286 return self.basepositioner.vbasepath(v1, v2)
288 def vgridpath(self, v):
289 return self.basepositioner.vgridpath(self.vmin+v*(self.vmax-self.vmin))
291 def vtickpoint_pt(self, v, axis=None):
292 return self.basepositioner.vtickpoint_pt(self.vmin+v*(self.vmax-self.vmin))
294 def vtickdirection(self, v, axis=None):
295 return self.basepositioner.vtickdirection(self.vmin+v*(self.vmax-self.vmin))
298 class bar(_axis):
300 def __init__(self, subaxes=None, defaultsubaxis=linear(painter=None, linkpainter=None, parter=None, texter=None),
301 dist=0.5, firstdist=None, lastdist=None, title=None, reverse=0,
302 painter=painter.bar(), linkpainter=painter.linkedbar()):
303 self.subaxes = subaxes
304 self.defaultsubaxis = defaultsubaxis
305 self.dist = dist
306 if firstdist is not None:
307 self.firstdist = firstdist
308 else:
309 self.firstdist = 0.5 * dist
310 if lastdist is not None:
311 self.lastdist = lastdist
312 else:
313 self.lastdist = 0.5 * dist
314 self.title = title
315 self.reverse = reverse
316 self.painter = painter
317 self.linkpainter = linkpainter
319 def createdata(self, errorname):
320 data = axisdata(size=self.firstdist+self.lastdist-self.dist, subaxes={}, names=[])
321 return data
323 def addsubaxis(self, data, name, subaxis, graphtexrunner, errorname):
324 subaxis = anchoredaxis(subaxis, graphtexrunner, "%s, subaxis %s" % (errorname, name))
325 subaxis.setcreatecall(lambda: None)
326 subaxis.sized = hasattr(subaxis.data, "size")
327 if subaxis.sized:
328 data.size += subaxis.data.size
329 else:
330 data.size += 1
331 data.size += self.dist
332 data.subaxes[name] = subaxis
333 if self.reverse:
334 data.names.insert(0, name)
335 else:
336 data.names.append(name)
338 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
339 for value in columndata:
341 # some checks and error messages
342 try:
343 len(value)
344 except:
345 raise ValueError("tuple expected by bar axis '%s'" % errorname)
346 try:
347 value + ""
348 except:
349 pass
350 else:
351 raise ValueError("tuple expected by bar axis '%s'" % errorname)
352 assert len(value) == 2, "tuple of size two expected by bar axis '%s'" % errorname
354 name = value[0]
355 if name is not None and name not in data.names:
356 if self.subaxes:
357 if self.subaxes[name] is not None:
358 self.addsubaxis(data, name, self.subaxes[name], graphtexrunner, errorname)
359 else:
360 self.addsubaxis(data, name, self.defaultsubaxis, graphtexrunner, errorname)
361 for name in data.names:
362 subaxis = data.subaxes[name]
363 if subaxis.sized:
364 data.size -= subaxis.data.size
365 subaxis.axis.adjustaxis(subaxis.data,
366 [value[1] for value in columndata if value[0] == name],
367 graphtexrunner,
368 "%s, subaxis %s" % (errorname, name))
369 if subaxis.sized:
370 data.size += subaxis.data.size
372 def convert(self, data, value):
373 if value[0] is None:
374 return None
375 axis = data.subaxes[value[0]]
376 vmin = axis.vmin
377 vmax = axis.vmax
378 return axis.vmin + axis.convert(value[1]) * (axis.vmax - axis.vmin)
380 def create(self, data, positioner, graphtexrunner, errorname):
381 canvas = painter.axiscanvas(self.painter, graphtexrunner)
382 v = 0
383 position = self.firstdist
384 for name in data.names:
385 subaxis = data.subaxes[name]
386 subaxis.vmin = position / float(data.size)
387 if subaxis.sized:
388 position += subaxis.data.size
389 else:
390 position += 1
391 subaxis.vmax = position / float(data.size)
392 position += 0.5*self.dist
393 subaxis.vminover = v
394 if name == data.names[-1]:
395 subaxis.vmaxover = 1
396 else:
397 subaxis.vmaxover = position / float(data.size)
398 subaxis.setpositioner(subaxispositioner(positioner, subaxis))
399 subaxis.create()
400 canvas.insert(subaxis.canvas)
401 if canvas.extent_pt < subaxis.canvas.extent_pt:
402 canvas.extent_pt = subaxis.canvas.extent_pt
403 position += 0.5*self.dist
404 v = subaxis.vmaxover
405 if self.painter is not None:
406 self.painter.paint(canvas, data, self, positioner)
407 return canvas
409 def createlinked(self, data, positioner, graphtexrunner, errorname, linkpainter):
410 canvas = painter.axiscanvas(self.painter, graphtexrunner)
411 for name in data.names:
412 subaxis = data.subaxes[name]
413 subaxis = linkedaxis(subaxis, name)
414 subaxis.setpositioner(subaxispositioner(positioner, data.subaxes[name]))
415 subaxis.create()
416 canvas.insert(subaxis.canvas)
417 if canvas.extent_pt < subaxis.canvas.extent_pt:
418 canvas.extent_pt = subaxis.canvas.extent_pt
419 if linkpainter is not None:
420 linkpainter.paint(canvas, data, self, positioner)
421 return canvas
424 class nestedbar(bar):
426 def __init__(self, defaultsubaxis=bar(dist=0, painter=None, linkpainter=None), **kwargs):
427 bar.__init__(self, defaultsubaxis=defaultsubaxis, **kwargs)
430 class split(bar):
432 def __init__(self, defaultsubaxis=linear(),
433 firstdist=0, lastdist=0,
434 painter=painter.split(), linkpainter=painter.linkedsplit(), **kwargs):
435 bar.__init__(self, defaultsubaxis=defaultsubaxis,
436 firstdist=firstdist, lastdist=lastdist,
437 painter=painter, linkpainter=linkpainter, **kwargs)
440 class sizedlinear(linear):
442 def __init__(self, size=1, **kwargs):
443 linear.__init__(self, **kwargs)
444 self.size = size
446 def createdata(self, errorname):
447 data = linear.createdata(self, errorname)
448 data.size = self.size
449 return data
451 sizedlin = sizedlinear
454 class autosizedlinear(linear):
456 def __init__(self, parter=parter.autolinear(extendtick=None), **kwargs):
457 linear.__init__(self, parter=parter, **kwargs)
459 def createdata(self, errorname):
460 data = linear.createdata(self, errorname)
461 try:
462 data.size = data.max - data.min
463 except:
464 data.size = 0
465 return data
467 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
468 linear.adjustaxis(self, data, columndata, graphtexrunner, errorname)
469 try:
470 data.size = data.max - data.min
471 except:
472 data.size = 0
474 def create(self, data, positioner, graphtexrunner, errorname):
475 min = data.min
476 max = data.max
477 canvas = linear.create(self, data, positioner, graphtexrunner, errorname)
478 if min != data.min or max != data.max:
479 raise RuntimeError("range change during axis creation of autosized linear axis")
480 return canvas
482 autosizedlin = autosizedlinear
485 class anchoredaxis:
487 def __init__(self, axis, graphtexrunner, errorname):
488 assert not isinstance(axis, anchoredaxis), errorname
489 self.axis = axis
490 self.errorname = errorname
491 self.graphtexrunner = graphtexrunner
492 self.data = axis.createdata(errorname)
493 self.canvas = None
494 self.positioner = None
496 def setcreatecall(self, function, *args, **kwargs):
497 self._createfunction = function
498 self._createargs = args
499 self._createkwargs = kwargs
501 def docreate(self):
502 if not self.canvas:
503 self._createfunction(*self._createargs, **self._createkwargs)
505 def setpositioner(self, positioner):
506 assert positioner is not None, self.errorname
507 assert self.positioner is None, self.errorname
508 self.positioner = positioner
510 def convert(self, x):
511 self.docreate()
512 return self.axis.convert(self.data, x)
514 def adjustaxis(self, columndata):
515 if self.canvas is None:
516 self.axis.adjustaxis(self.data, columndata, self.graphtexrunner, self.errorname)
517 else:
518 warnings.warn("ignore axis range adjustment of already created axis '%s'" % self.errorname)
520 def vbasepath(self, v1=None, v2=None):
521 return self.positioner.vbasepath(v1=v1, v2=v2)
523 def basepath(self, x1=None, x2=None):
524 self.docreate()
525 if x1 is None:
526 if x2 is None:
527 return self.positioner.vbasepath()
528 else:
529 return self.positioner.vbasepath(v2=self.axis.convert(self.data, x2))
530 else:
531 if x2 is None:
532 return self.positioner.vbasepath(v1=self.axis.convert(self.data, x1))
533 else:
534 return self.positioner.vbasepath(v1=self.axis.convert(self.data, x1),
535 v2=self.axis.convert(self.data, x2))
537 def vgridpath(self, v):
538 return self.positioner.vgridpath(v)
540 def gridpath(self, x):
541 self.docreate()
542 return self.positioner.vgridpath(self.axis.convert(self.data, x))
544 def vtickpoint_pt(self, v):
545 return self.positioner.vtickpoint_pt(v)
547 def vtickpoint(self, v):
548 return self.positioner.vtickpoint_pt(v) * unit.t_pt
550 def tickpoint_pt(self, x):
551 self.docreate()
552 return self.positioner.vtickpoint_pt(self.axis.convert(self.data, x))
554 def tickpoint(self, x):
555 self.docreate()
556 x_pt, y_pt = self.positioner.vtickpoint_pt(self.axis.convert(self.data, x))
557 return x_pt * unit.t_pt, y_pt * unit.t_pt
559 def vtickdirection(self, v):
560 return self.positioner.vtickdirection(v)
562 def tickdirection(self, x):
563 self.docreate()
564 return self.positioner.vtickdirection(self.axis.convert(self.data, x))
566 def create(self):
567 if self.canvas is None:
568 assert self.positioner is not None, self.errorname
569 self.canvas = self.axis.create(self.data, self.positioner, self.graphtexrunner, self.errorname)
570 return self.canvas
573 class linkedaxis(anchoredaxis):
575 def __init__(self, linkedaxis=None, errorname="manual-linked", painter=_marker):
576 self.painter = painter
577 self.linkedto = None
578 self.errorname = errorname
579 self.canvas = None
580 self.positioner = None
581 if linkedaxis:
582 self.setlinkedaxis(linkedaxis)
584 def setlinkedaxis(self, linkedaxis):
585 assert isinstance(linkedaxis, anchoredaxis), errorname
586 self.linkedto = linkedaxis
587 self.axis = linkedaxis.axis
588 self.graphtexrunner = self.linkedto.graphtexrunner
589 self.errorname = "%s (linked to %s)" % (self.errorname, linkedaxis.errorname)
590 self.data = linkedaxis.data
591 if self.painter is _marker:
592 self.painter = linkedaxis.axis.linkpainter
594 def create(self):
595 assert self.linkedto is not None, self.errorname
596 assert self.positioner is not None, self.errorname
597 if self.canvas is None:
598 self.linkedto.docreate()
599 self.canvas = self.axis.createlinked(self.data, self.positioner, self.graphtexrunner, self.errorname, self.painter)
600 return self.canvas
603 class anchoredpathaxis(anchoredaxis):
604 """an anchored axis along a path"""
606 def __init__(self, path, axis, **kwargs):
607 anchoredaxis.__init__(self, axis, text.defaulttexrunner, "pathaxis")
608 self.setpositioner(positioner.pathpositioner(path, **kwargs))
609 self.create()
611 def pathaxis(*args, **kwargs):
612 """creates an axiscanvas for an axis along a path"""
613 return anchoredpathaxis(*args, **kwargs).canvas