callback rename into docreate
[PyX/mjg.git] / pyx / graph / axis / axis.py
blob3c5353d232830eda896bb0d39510b58a3f635d32
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
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 _linmap:
64 "linear conversion methods"
66 def convert(self, data, value):
67 """axis coordinates -> graph coordinates"""
68 if self.reverse:
69 return (data.max - float(value)) / (data.max - data.min)
70 else:
71 return (float(value) - data.min) / (data.max - data.min)
74 class _logmap:
75 "logarithmic convertion methods"
77 def convert(self, data, value):
78 """axis coordinates -> graph coordinates"""
79 # TODO: store log(data.min) and log(data.max)
80 if self.reverse:
81 return (math.log(data.max) - math.log(float(value))) / (math.log(data.max) - math.log(data.min))
82 else:
83 return (math.log(float(value)) - math.log(data.min)) / (math.log(data.max) - math.log(data.min))
86 class _regularaxis(_axis):
87 """base implementation a regular axis
89 Regular axis have a continuous variable like linear axes,
90 logarithmic axes, time axes etc."""
92 def __init__(self, min=None, max=None, reverse=0, divisor=None, title=None,
93 painter=painter.regular(), texter=texter.mixed(), linkpainter=painter.linked(),
94 density=1, maxworse=2, manualticks=[]):
95 if min is not None and max is not None and min > max:
96 min, max, reverse = max, min, not reverse
97 self.min = min
98 self.max = max
99 self.reverse = reverse
100 self.divisor = divisor
101 self.title = title
102 self.painter = painter
103 self.texter = texter
104 self.linkpainter = linkpainter
105 self.density = density
106 self.maxworse = maxworse
107 self.manualticks = self.checkfraclist(manualticks)
109 def createdata(self, errorname):
110 return axisdata(min=self.min, max=self.max)
112 zero = 0.0
114 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
115 if self.min is None or self.max is None:
116 for value in columndata:
117 try:
118 value = value + self.zero
119 except:
120 pass
121 else:
122 if self.min is None and (data.min is None or value < data.min):
123 data.min = value
124 if self.max is None and (data.max is None or value > data.max):
125 data.max = value
127 def checkfraclist(self, fracs):
128 "orders a list of fracs, equal entries are not allowed"
129 if not len(fracs): return []
130 sorted = list(fracs)
131 sorted.sort()
132 last = sorted[0]
133 for item in sorted[1:]:
134 if last == item:
135 raise ValueError("duplicate entry found")
136 last = item
137 return sorted
139 def create(self, data, positioner, graphtexrunner, errorname):
140 errorname = " for axis %s" % errorname
141 if data.min is None or data.max is None:
142 raise RuntimeError("incomplete axis range%s" % errorname)
144 # a variant is a data copy with local modifications to test several partitions
145 class variant:
146 def __init__(self, data, **kwargs):
147 self.data = data
148 for key, value in kwargs.items():
149 setattr(self, key, value)
151 def __getattr__(self, key):
152 return getattr(data, key)
154 def __cmp__(self, other):
155 return cmp(self.rate, other.rate)
157 # build a list of variants
158 bestrate = None
159 if self.parter is not None:
160 if self.divisor is not None:
161 partfunctions = self.parter.partfunctions(data.min/self.divisor, data.max/self.divisor,
162 self.min is None, self.max is None)
163 else:
164 partfunctions = self.parter.partfunctions(data.min, data.max,
165 self.min is None, self.max is None)
166 variants = []
167 for partfunction in partfunctions:
168 worse = 0
169 while worse < self.maxworse:
170 worse += 1
171 ticks = partfunction()
172 if ticks is None:
173 break
174 ticks = tick.mergeticklists(self.manualticks, ticks, mergeequal=0)
175 if ticks:
176 rate = ( self.rater.rateticks(self, ticks, self.density) +
177 self.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))
183 if not variants:
184 raise RuntimeError("no axis partitioning found%s" % errorname)
185 else:
186 variants = [variant(data, rate=0, ticks=self.manualticks)]
188 # get best variant
189 if self.painter is None or len(variants) == 1:
190 # in case of a single variant we're almost done
191 self.adjustaxis(data, variants[0].ticks, graphtexrunner, errorname)
192 if variants[0].ticks:
193 self.texter.labels(variants[0].ticks)
194 if self.divisor:
195 for t in variants[0].ticks:
196 t *= tick.rational(self.divisor)
197 canvas = painter.axiscanvas(self.painter, graphtexrunner)
198 data.ticks = variants[0].ticks
199 if self.painter is not None:
200 self.painter.paint(canvas, data, self, positioner)
201 return canvas
202 else:
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 self.adjustaxis(variants[0], variants[0].ticks, graphtexrunner, errorname)
209 if variants[0].ticks:
210 self.texter.labels(variants[0].ticks)
211 if self.divisor:
212 for t in variants[0].ticks:
213 t *= tick.rational(self.divisor)
214 canvas = painter.axiscanvas(self.painter, graphtexrunner)
215 self.painter.paint(canvas, variants[0], self, positioner)
216 ratelayout = self.rater.ratelayout(canvas, self.density)
217 if ratelayout is None:
218 del variants[0]
219 if not variants:
220 raise RuntimeError("no valid axis partitioning found%s" % errorname)
221 else:
222 variants[0].rate += ratelayout
223 variants[0].storedcanvas = canvas
224 variants.sort()
225 self.adjustaxis(data, variants[0].ticks, graphtexrunner, errorname)
226 data.ticks = variants[0].ticks
227 return variants[0].storedcanvas
230 class linear(_regularaxis, _linmap):
231 """linear axis"""
233 def __init__(self, parter=parter.autolinear(), rater=rater.linear(), **args):
234 _regularaxis.__init__(self, **args)
235 self.parter = parter
236 self.rater = rater
238 lin = linear
241 class logarithmic(_regularaxis, _logmap):
242 """logarithmic axis"""
244 def __init__(self, parter=parter.autologarithmic(), rater=rater.logarithmic(), **args):
245 _regularaxis.__init__(self, **args)
246 self.parter = parter
247 self.rater = rater
249 log = logarithmic
252 class subaxispositioner(positioner._positioner):
253 """a subaxis positioner"""
255 def __init__(self, basepositioner, subaxis):
256 self.basepositioner = basepositioner
257 self.vmin = subaxis.vmin
258 self.vmax = subaxis.vmax
259 self.vminover = subaxis.vminover
260 self.vmaxover = subaxis.vmaxover
262 def vbasepath(self, v1=None, v2=None):
263 if v1 is not None:
264 v1 = self.vmin+v1*(self.vmax-self.vmin)
265 else:
266 v1 = self.vminover
267 if v2 is not None:
268 v2 = self.vmin+v2*(self.vmax-self.vmin)
269 else:
270 v2 = self.vmaxover
271 return self.basepositioner.vbasepath(v1, v2)
273 def vgridpath(self, v):
274 return self.basepositioner.vgridpath(self.vmin+v*(self.vmax-self.vmin))
276 def vtickpoint_pt(self, v, axis=None):
277 return self.basepositioner.vtickpoint_pt(self.vmin+v*(self.vmax-self.vmin))
279 def vtickdirection(self, v, axis=None):
280 return self.basepositioner.vtickdirection(self.vmin+v*(self.vmax-self.vmin))
283 class bar(_axis):
285 def __init__(self, subaxes=None, defaultsubaxis=linear(painter=None, linkpainter=None, parter=None, texter=None),
286 dist=0.5, firstdist=None, lastdist=None, title=None, reverse=0,
287 painter=painter.bar(), linkpainter=painter.linkedbar()):
288 self.subaxes = subaxes
289 self.defaultsubaxis = defaultsubaxis
290 self.dist = dist
291 if firstdist is not None:
292 self.firstdist = firstdist
293 else:
294 self.firstdist = 0.5 * dist
295 if lastdist is not None:
296 self.lastdist = lastdist
297 else:
298 self.lastdist = 0.5 * dist
299 self.title = title
300 self.reverse = reverse
301 self.painter = painter
302 self.linkpainter = linkpainter
304 def createdata(self, errorname):
305 data = axisdata(size=self.firstdist+self.lastdist-self.dist, subaxes={}, names=[])
306 return data
308 def addsubaxis(self, data, name, subaxis, graphtexrunner, errorname):
309 subaxis = anchoredaxis(subaxis, graphtexrunner, "%s, subaxis %s" % (errorname, name))
310 subaxis.sized = hasattr(subaxis.data, "size")
311 if subaxis.sized:
312 data.size += subaxis.data.size
313 else:
314 data.size += 1
315 data.size += self.dist
316 data.subaxes[name] = subaxis
317 if self.reverse:
318 data.names.insert(0, name)
319 else:
320 data.names.append(name)
322 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
323 for value in columndata:
325 # some checks and error messages
326 try:
327 len(value)
328 except:
329 raise ValueError("tuple expected by bar axis '%s'" % errorname)
330 try:
331 value + ""
332 except:
333 pass
334 else:
335 raise ValueError("tuple expected by bar axis '%s'" % errorname)
336 assert len(value) == 2, "tuple of size two expected by bar axis '%s'" % errorname
338 name = value[0]
339 if name is not None and name not in data.names:
340 if self.subaxes:
341 if self.subaxes[name] is not None:
342 self.addsubaxis(data, name, self.subaxes[name], graphtexrunner, errorname)
343 else:
344 self.addsubaxis(data, name, self.defaultsubaxis, graphtexrunner, errorname)
345 for name in data.names:
346 subaxis = data.subaxes[name]
347 if subaxis.sized:
348 data.size -= subaxis.data.size
349 subaxis.axis.adjustaxis(subaxis.data,
350 [value[1] for value in columndata if value[0] == name],
351 graphtexrunner,
352 "%s, subaxis %s" % (errorname, name))
353 if subaxis.sized:
354 data.size += subaxis.data.size
356 def convert(self, data, value):
357 if value[0] is None:
358 return None
359 axis = data.subaxes[value[0]]
360 vmin = axis.vmin
361 vmax = axis.vmax
362 return axis.vmin + axis.convert(value[1]) * (axis.vmax - axis.vmin)
364 def create(self, data, positioner, graphtexrunner, errorname):
365 canvas = painter.axiscanvas(self.painter, graphtexrunner)
366 v = 0
367 position = self.firstdist
368 for name in data.names:
369 subaxis = data.subaxes[name]
370 subaxis.vmin = position / float(data.size)
371 if subaxis.sized:
372 position += subaxis.data.size
373 else:
374 position += 1
375 subaxis.vmax = position / float(data.size)
376 position += 0.5*self.dist
377 subaxis.vminover = v
378 if name == data.names[-1]:
379 subaxis.vmaxover = 1
380 else:
381 subaxis.vmaxover = position / float(data.size)
382 subaxis.setpositioner(subaxispositioner(positioner, subaxis))
383 subaxis.create()
384 canvas.insert(subaxis.canvas)
385 if canvas.extent_pt < subaxis.canvas.extent_pt:
386 canvas.extent_pt = subaxis.canvas.extent_pt
387 position += 0.5*self.dist
388 v = subaxis.vmaxover
389 if self.painter is not None:
390 self.painter.paint(canvas, data, self, positioner)
391 return canvas
393 def createlinked(self, data, positioner, graphtexrunner, errorname, linkpainter):
394 canvas = painter.axiscanvas(self.painter, graphtexrunner)
395 for name in data.names:
396 subaxis = data.subaxes[name]
397 subaxis = linkedaxis(subaxis, name)
398 subaxis.setpositioner(subaxispositioner(positioner, data.subaxes[name]))
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 if linkpainter is not None:
404 linkpainter.paint(canvas, data, self, positioner)
405 return canvas
408 class nestedbar(bar):
410 def __init__(self, defaultsubaxis=bar(dist=0, painter=None, linkpainter=None), **kwargs):
411 bar.__init__(self, defaultsubaxis=defaultsubaxis, **kwargs)
414 class split(bar):
416 def __init__(self, defaultsubaxis=linear(),
417 firstdist=0, lastdist=0,
418 painter=painter.split(), linkpainter=painter.linkedsplit(), **kwargs):
419 bar.__init__(self, defaultsubaxis=defaultsubaxis,
420 firstdist=firstdist, lastdist=lastdist,
421 painter=painter, linkpainter=linkpainter, **kwargs)
424 class sizedlinear(linear):
426 def __init__(self, size=1, **kwargs):
427 linear.__init__(self, **kwargs)
428 self.size = size
430 def createdata(self, errorname):
431 data = linear.createdata(self, errorname)
432 data.size = self.size
433 return data
435 sizedlin = sizedlinear
438 class autosizedlinear(linear):
440 def __init__(self, parter=parter.autolinear(extendtick=None), **kwargs):
441 linear.__init__(self, parter=parter, **kwargs)
443 def createdata(self, errorname):
444 data = linear.createdata(self, errorname)
445 try:
446 data.size = data.max - data.min
447 except:
448 data.size = 0
449 return data
451 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
452 linear.adjustaxis(self, data, columndata, graphtexrunner, errorname)
453 try:
454 data.size = data.max - data.min
455 except:
456 data.size = 0
458 def create(self, data, positioner, graphtexrunner, errorname):
459 min = data.min
460 max = data.max
461 canvas = linear.create(self, data, positioner, graphtexrunner, errorname)
462 if min != data.min or max != data.max:
463 raise RuntimeError("range change during axis creation of autosized linear axis")
464 return canvas
466 autosizedlin = autosizedlinear
469 class anchoredaxis:
471 def __init__(self, axis, graphtexrunner, errorname):
472 assert not isinstance(axis, anchoredaxis), errorname
473 self.axis = axis
474 self.errorname = errorname
475 self.graphtexrunner = graphtexrunner
476 self.data = axis.createdata(errorname)
477 self.canvas = None
478 self.positioner = None
480 def setcreatecall(self, function, *args, **kwargs):
481 self._createfunction = function
482 self._createargs = args
483 self._createkwargs = kwargs
485 def docreate(self):
486 if not self.canvas:
487 self._createfunction(*self._createargs, **self._createkwargs)
489 def setpositioner(self, positioner):
490 assert positioner is not None, self.errorname
491 assert self.positioner is None, self.errorname
492 self.positioner = positioner
494 def convert(self, x):
495 self.docreate()
496 return self.axis.convert(self.data, x)
498 def adjustaxis(self, columndata):
499 if self.canvas is None:
500 self.axis.adjustaxis(self.data, columndata, self.graphtexrunner, self.errorname)
501 else:
502 warnings.warn("ignore axis range adjustment of already created axis '%s'" % self.errorname)
504 def vbasepath(self, v1=None, v2=None):
505 return self.positioner.vbasepath(v1=v1, v2=v2)
507 def basepath(self, x1=None, x2=None):
508 self.docreate()
509 if x1 is None:
510 if x2 is None:
511 return self.positioner.vbasepath()
512 else:
513 return self.positioner.vbasepath(v2=self.axis.convert(self.data, x2))
514 else:
515 if x2 is None:
516 return self.positioner.vbasepath(v1=self.axis.convert(self.data, x1))
517 else:
518 return self.positioner.vbasepath(v1=self.axis.convert(self.data, x1),
519 v2=self.axis.convert(self.data, x2))
521 def vgridpath(self, v):
522 return self.positioner.vgridpath(v)
524 def gridpath(self, x):
525 self.docreate()
526 return self.positioner.vgridpath(self.axis.convert(self.data, x))
528 def vtickpoint_pt(self, v):
529 return self.positioner.vtickpoint_pt(v)
531 def vtickpoint(self, v):
532 return self.positioner.vtickpoint_pt(v) * unit.t_pt
534 def tickpoint_pt(self, x):
535 self.docreate()
536 return self.positioner.vtickpoint_pt(self.axis.convert(self.data, x))
538 def tickpoint(self, x):
539 self.docreate()
540 x_pt, y_pt = self.positioner.vtickpoint_pt(self.axis.convert(self.data, x))
541 return x_pt * unit.t_pt, y_pt * unit.t_pt
543 def vtickdirection(self, v):
544 return self.positioner.vtickdirection(v)
546 def tickdirection(self, x):
547 self.docreate()
548 return self.positioner.vtickdirection(self.axis.convert(self.data, x))
550 def create(self):
551 if self.canvas is None:
552 assert self.positioner is not None, self.errorname
553 self.canvas = self.axis.create(self.data, self.positioner, self.graphtexrunner, self.errorname)
554 return self.canvas
557 class linkedaxis(anchoredaxis):
559 def __init__(self, linkedaxis=None, errorname="manual-linked", painter=_marker):
560 self.painter = painter
561 self.linkedto = None
562 self.errorname = errorname
563 self.canvas = None
564 self.positioner = None
565 if linkedaxis:
566 self.setlinkedaxis(linkedaxis)
568 def setlinkedaxis(self, linkedaxis):
569 assert isinstance(linkedaxis, anchoredaxis), errorname
570 self.linkedto = linkedaxis
571 self.axis = linkedaxis.axis
572 self.graphtexrunner = self.linkedto.graphtexrunner
573 self.errorname = "%s (linked to %s)" % (self.errorname, linkedaxis.errorname)
574 self.data = linkedaxis.data
575 if self.painter is _marker:
576 self.painter = linkedaxis.axis.linkpainter
578 def create(self):
579 assert self.linkedto is not None, self.errorname
580 assert self.positioner is not None, self.errorname
581 if self.canvas is None:
582 self.linkedto.docreate()
583 self.canvas = self.axis.createlinked(self.data, self.positioner, self.graphtexrunner, self.errorname, self.painter)
584 return self.canvas
587 class anchoredpathaxis(anchoredaxis):
588 """an anchored axis along a path"""
590 def __init__(self, path, axis, **kwargs):
591 anchoredaxis.__init__(self, axis, text.defaulttexrunner, "pathaxis")
592 self.setpositioner(positioner.pathpositioner(path, **kwargs))
593 self.create()
595 def pathaxis(*args, **kwargs):
596 """creates an axiscanvas for an axis along a path"""
597 return anchoredpathaxis(*args, **kwargs).canvas