add UnicodeEngine (MultiEngineText and axis texters returning MultiEngineText), texte...
[PyX.git] / pyx / graph / axis / axis.py
blobce52caede1e696d27e323f46d784cb5dacc240d7
1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2002-2012 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2011 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2012 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 functools, logging, math
25 from pyx import attr, unit, text
26 from pyx.graph.axis import painter, parter, positioner, rater, texter, tick
28 logger = logging.getLogger("pyx")
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 list(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.default(), 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 if self.divisor is not None:
127 rational_divisor = tick.rational(self.divisor)
128 convert_tick = lambda x: float(x)*self.divisor
129 else:
130 convert_tick = lambda x: x
132 def layout(data):
133 if data.ticks:
134 self.adjustaxis(data, [convert_tick(data.ticks[0]), convert_tick(data.ticks[-1])], graphtexrunner, errorname)
135 self.texter.labels(data.ticks)
136 if self.divisor:
137 for t in data.ticks:
138 t *= rational_divisor
139 canvas = painter.axiscanvas(self.painter, graphtexrunner)
140 if self.painter is not None:
141 self.painter.paint(canvas, data, self, positioner)
142 return canvas
144 if parter is None:
145 data.ticks = self.manualticks
146 return layout(data)
148 # a variant is a data copy with local modifications to test several partitions
149 @functools.total_ordering
150 class variant:
151 def __init__(self, data, **kwargs):
152 self.data = data
153 for key, value in list(kwargs.items()):
154 setattr(self, key, value)
156 def __getattr__(self, key):
157 return getattr(data, key)
159 def __lt__(self, other):
160 # we can also sort variants by their rate
161 return self.rate < other.rate
163 def __eq__(self, other):
164 # we can also sort variants by their rate
165 return self.rate == other.rate
167 # build a list of variants
168 bestrate = None
169 if self.divisor is not None:
170 if data.min is not None:
171 data_min_divided = data.min/self.divisor
172 else:
173 data_min_divided = None
174 if data.max is not None:
175 data_max_divided = data.max/self.divisor
176 else:
177 data_max_divided = None
178 partfunctions = parter.partfunctions(data_min_divided, data_max_divided,
179 self.min is None, self.max is None)
180 else:
181 partfunctions = parter.partfunctions(data.min, data.max,
182 self.min is None, self.max is None)
183 variants = []
184 for partfunction in partfunctions:
185 worse = 0
186 while worse < self.maxworse:
187 worse += 1
188 ticks = partfunction()
189 if ticks is None:
190 break
191 ticks = tick.mergeticklists(self.manualticks, ticks, mergeequal=0)
192 if ticks:
193 rate = rater.rateticks(self, ticks, self.density)
194 if rate is not None:
195 if self.reverse:
196 rate += rater.raterange(self.convert(data, convert_tick(ticks[0])) -
197 self.convert(data, convert_tick(ticks[-1])), 1)
198 else:
199 rate += rater.raterange(self.convert(data, convert_tick(ticks[-1])) -
200 self.convert(data, convert_tick(ticks[0])), 1)
201 if bestrate is None or rate < bestrate:
202 bestrate = rate
203 worse = 0
204 variants.append(variant(data, rate=rate, ticks=ticks))
206 if not variants:
207 raise RuntimeError("no axis partitioning found%s" % errorname)
209 if len(variants) == 1 or self.painter is None:
210 # When the painter is None, we could sort the variants here by their rating.
211 # However, we didn't did this so far and there is no real reason to change that.
212 data.ticks = variants[0].ticks
213 return layout(data)
215 # build the layout for best variants
216 for variant in variants:
217 variant.storedcanvas = None
218 variants.sort()
219 while not variants[0].storedcanvas:
220 variants[0].storedcanvas = layout(variants[0])
221 ratelayout = rater.ratelayout(variants[0].storedcanvas, self.density)
222 if ratelayout is None:
223 del variants[0]
224 if not variants:
225 raise NoValidPartitionError("no valid axis partitioning found%s" % errorname)
226 else:
227 variants[0].rate += ratelayout
228 variants.sort()
229 self.adjustaxis(data, variants[0].ticks, graphtexrunner, errorname)
230 data.ticks = variants[0].ticks
231 return variants[0].storedcanvas
234 class linear(_regularaxis):
235 """linear axis"""
237 def __init__(self, parter=parter.autolinear(), rater=rater.linear(), **args):
238 _regularaxis.__init__(self, **args)
239 self.parter = parter
240 self.rater = rater
242 def convert(self, data, value):
243 """axis coordinates -> graph coordinates"""
244 if self.reverse:
245 return (data.max - float(value)) / (data.max - data.min)
246 else:
247 return (float(value) - data.min) / (data.max - data.min)
249 def create(self, data, positioner, graphtexrunner, errorname):
250 return _regularaxis._create(self, data, positioner, graphtexrunner, self.parter, self.rater, errorname)
252 lin = linear
255 class logarithmic(_regularaxis):
256 """logarithmic axis"""
258 def __init__(self, parter=parter.autologarithmic(), rater=rater.logarithmic(),
259 linearparter=parter.autolinear(extendtick=None), **args):
260 _regularaxis.__init__(self, **args)
261 self.parter = parter
262 self.rater = rater
263 self.linearparter = linearparter
265 def convert(self, data, value):
266 """axis coordinates -> graph coordinates"""
267 # TODO: store log(data.min) and log(data.max)
268 if self.reverse:
269 return (math.log(data.max) - math.log(float(value))) / (math.log(data.max) - math.log(data.min))
270 else:
271 return (math.log(float(value)) - math.log(data.min)) / (math.log(data.max) - math.log(data.min))
273 def create(self, data, positioner, graphtexrunner, errorname):
274 try:
275 return _regularaxis._create(self, data, positioner, graphtexrunner, self.parter, self.rater, errorname)
276 except NoValidPartitionError:
277 if self.linearparter:
278 logger.warning("no valid logarithmic partitioning found for axis %s, switch to linear partitioning" % errorname)
279 return _regularaxis._create(self, data, positioner, graphtexrunner, self.linearparter, self.rater, errorname)
280 raise
282 log = logarithmic
285 class subaxispositioner(positioner._positioner):
286 """a subaxis positioner"""
288 def __init__(self, basepositioner, subaxis):
289 self.basepositioner = basepositioner
290 self.vmin = subaxis.vmin
291 self.vmax = subaxis.vmax
292 self.vminover = subaxis.vminover
293 self.vmaxover = subaxis.vmaxover
295 def vbasepath(self, v1=None, v2=None):
296 if v1 is not None:
297 v1 = self.vmin+v1*(self.vmax-self.vmin)
298 else:
299 v1 = self.vminover
300 if v2 is not None:
301 v2 = self.vmin+v2*(self.vmax-self.vmin)
302 else:
303 v2 = self.vmaxover
304 return self.basepositioner.vbasepath(v1, v2)
306 def vgridpath(self, v):
307 return self.basepositioner.vgridpath(self.vmin+v*(self.vmax-self.vmin))
309 def vtickpoint_pt(self, v, axis=None):
310 return self.basepositioner.vtickpoint_pt(self.vmin+v*(self.vmax-self.vmin))
312 def vtickdirection(self, v, axis=None):
313 return self.basepositioner.vtickdirection(self.vmin+v*(self.vmax-self.vmin))
316 class bar(_axis):
318 def __init__(self, subaxes=None, defaultsubaxis=linear(painter=None, linkpainter=None, parter=None),
319 dist=0.5, firstdist=None, lastdist=None, title=None, reverse=0,
320 painter=painter.bar(), linkpainter=painter.linkedbar()):
321 self.subaxes = subaxes
322 self.defaultsubaxis = defaultsubaxis
323 self.dist = dist
324 if firstdist is not None:
325 self.firstdist = firstdist
326 else:
327 self.firstdist = 0.5 * dist
328 if lastdist is not None:
329 self.lastdist = lastdist
330 else:
331 self.lastdist = 0.5 * dist
332 self.title = title
333 self.reverse = reverse
334 self.painter = painter
335 self.linkpainter = linkpainter
337 def createdata(self, errorname):
338 data = axisdata(size=self.firstdist+self.lastdist-self.dist, subaxes={}, names=[])
339 return data
341 def addsubaxis(self, data, name, subaxis, graphtexrunner, errorname):
342 subaxis = anchoredaxis(subaxis, graphtexrunner, "%s, subaxis %s" % (errorname, name))
343 subaxis.setcreatecall(lambda: None)
344 subaxis.sized = hasattr(subaxis.data, "size")
345 if subaxis.sized:
346 data.size += subaxis.data.size
347 else:
348 data.size += 1
349 data.size += self.dist
350 data.subaxes[name] = subaxis
351 if self.reverse:
352 data.names.insert(0, name)
353 else:
354 data.names.append(name)
356 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
357 for value in columndata:
359 # some checks and error messages
360 try:
361 len(value)
362 except:
363 raise ValueError("tuple expected by bar axis '%s'" % errorname)
364 try:
365 value + ""
366 except:
367 pass
368 else:
369 raise ValueError("tuple expected by bar axis '%s'" % errorname)
370 assert len(value) == 2, "tuple of size two expected by bar axis '%s'" % errorname
372 name = value[0]
373 if name is not None and name not in data.names:
374 if self.subaxes:
375 if self.subaxes[name] is not None:
376 self.addsubaxis(data, name, self.subaxes[name], graphtexrunner, errorname)
377 else:
378 self.addsubaxis(data, name, self.defaultsubaxis, graphtexrunner, errorname)
379 for name in data.names:
380 subaxis = data.subaxes[name]
381 if subaxis.sized:
382 data.size -= subaxis.data.size
383 subaxis.axis.adjustaxis(subaxis.data,
384 [value[1] for value in columndata if value[0] == name],
385 graphtexrunner,
386 "%s, subaxis %s" % (errorname, name))
387 if subaxis.sized:
388 data.size += subaxis.data.size
390 def convert(self, data, value):
391 if value[0] is None:
392 raise ValueError
393 axis = data.subaxes[value[0]]
394 vmin = axis.vmin
395 vmax = axis.vmax
396 return axis.vmin + axis.convert(value[1]) * (axis.vmax - axis.vmin)
398 def create(self, data, positioner, graphtexrunner, errorname):
399 canvas = painter.axiscanvas(self.painter, graphtexrunner)
400 v = 0
401 position = self.firstdist
402 for name in data.names:
403 subaxis = data.subaxes[name]
404 subaxis.vmin = position / float(data.size)
405 if subaxis.sized:
406 position += subaxis.data.size
407 else:
408 position += 1
409 subaxis.vmax = position / float(data.size)
410 position += 0.5*self.dist
411 subaxis.vminover = v
412 if name == data.names[-1]:
413 subaxis.vmaxover = 1
414 else:
415 subaxis.vmaxover = position / float(data.size)
416 subaxis.setpositioner(subaxispositioner(positioner, subaxis))
417 subaxis.create()
418 for layer, subcanvas in list(subaxis.canvas.layers.items()):
419 canvas.layer(layer).insert(subcanvas)
420 assert len(subaxis.canvas.layers) == len(subaxis.canvas.items)
421 if canvas.extent_pt < subaxis.canvas.extent_pt:
422 canvas.extent_pt = subaxis.canvas.extent_pt
423 position += 0.5*self.dist
424 v = subaxis.vmaxover
425 if self.painter is not None:
426 self.painter.paint(canvas, data, self, positioner)
427 return canvas
429 def createlinked(self, data, positioner, graphtexrunner, errorname, linkpainter):
430 canvas = painter.axiscanvas(self.painter, graphtexrunner)
431 for name in data.names:
432 subaxis = data.subaxes[name]
433 subaxis = linkedaxis(subaxis, name)
434 subaxis.setpositioner(subaxispositioner(positioner, data.subaxes[name]))
435 subaxis.create()
436 for layer, subcanvas in list(subaxis.canvas.layers.items()):
437 canvas.layer(layer).insert(subcanvas)
438 assert len(subaxis.canvas.layers) == len(subaxis.canvas.items)
439 if canvas.extent_pt < subaxis.canvas.extent_pt:
440 canvas.extent_pt = subaxis.canvas.extent_pt
441 if linkpainter is not None:
442 linkpainter.paint(canvas, data, self, positioner)
443 return canvas
446 class nestedbar(bar):
448 def __init__(self, defaultsubaxis=bar(dist=0, painter=None, linkpainter=None), **kwargs):
449 bar.__init__(self, defaultsubaxis=defaultsubaxis, **kwargs)
452 class split(bar):
454 def __init__(self, defaultsubaxis=linear(),
455 firstdist=0, lastdist=0,
456 painter=painter.split(), linkpainter=painter.linkedsplit(), **kwargs):
457 bar.__init__(self, defaultsubaxis=defaultsubaxis,
458 firstdist=firstdist, lastdist=lastdist,
459 painter=painter, linkpainter=linkpainter, **kwargs)
462 class sizedlinear(linear):
464 def __init__(self, size=1, **kwargs):
465 linear.__init__(self, **kwargs)
466 self.size = size
468 def createdata(self, errorname):
469 data = linear.createdata(self, errorname)
470 data.size = self.size
471 return data
473 sizedlin = sizedlinear
476 class autosizedlinear(linear):
478 def __init__(self, parter=parter.autolinear(extendtick=None), **kwargs):
479 linear.__init__(self, parter=parter, **kwargs)
481 def createdata(self, errorname):
482 data = linear.createdata(self, errorname)
483 try:
484 data.size = data.max - data.min
485 except:
486 data.size = 0
487 return data
489 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
490 linear.adjustaxis(self, data, columndata, graphtexrunner, errorname)
491 try:
492 data.size = data.max - data.min
493 except:
494 data.size = 0
496 def create(self, data, positioner, graphtexrunner, errorname):
497 min = data.min
498 max = data.max
499 canvas = linear.create(self, data, positioner, graphtexrunner, errorname)
500 if min != data.min or max != data.max:
501 raise RuntimeError("range change during axis creation of autosized linear axis")
502 return canvas
504 autosizedlin = autosizedlinear
507 class anchoredaxis:
509 def __init__(self, axis, graphtexrunner, errorname):
510 assert not isinstance(axis, anchoredaxis), errorname
511 self.axis = axis
512 self.errorname = errorname
513 self.graphtexrunner = graphtexrunner
514 self.data = axis.createdata(self.errorname)
515 self.canvas = None
516 self.positioner = None
518 def setcreatecall(self, function, *args, **kwargs):
519 self._createfunction = function
520 self._createargs = args
521 self._createkwargs = kwargs
523 def docreate(self):
524 if not self.canvas:
525 self._createfunction(*self._createargs, **self._createkwargs)
527 def setpositioner(self, positioner):
528 assert positioner is not None, self.errorname
529 assert self.positioner is None, self.errorname
530 self.positioner = positioner
532 def convert(self, x):
533 self.docreate()
534 return self.axis.convert(self.data, x)
536 def adjustaxis(self, columndata):
537 if self.canvas is None:
538 self.axis.adjustaxis(self.data, columndata, self.graphtexrunner, self.errorname)
539 else:
540 logger.warning("ignore axis range adjustment of already created axis '%s'" % self.errorname)
542 def vbasepath(self, v1=None, v2=None):
543 return self.positioner.vbasepath(v1=v1, v2=v2)
545 def basepath(self, x1=None, x2=None):
546 self.docreate()
547 if x1 is None:
548 if x2 is None:
549 return self.positioner.vbasepath()
550 else:
551 return self.positioner.vbasepath(v2=self.axis.convert(self.data, x2))
552 else:
553 if x2 is None:
554 return self.positioner.vbasepath(v1=self.axis.convert(self.data, x1))
555 else:
556 return self.positioner.vbasepath(v1=self.axis.convert(self.data, x1),
557 v2=self.axis.convert(self.data, x2))
559 def vgridpath(self, v):
560 return self.positioner.vgridpath(v)
562 def gridpath(self, x):
563 self.docreate()
564 return self.positioner.vgridpath(self.axis.convert(self.data, x))
566 def vtickpoint_pt(self, v):
567 return self.positioner.vtickpoint_pt(v)
569 def vtickpoint(self, v):
570 return self.positioner.vtickpoint_pt(v) * unit.t_pt
572 def tickpoint_pt(self, x):
573 self.docreate()
574 return self.positioner.vtickpoint_pt(self.axis.convert(self.data, x))
576 def tickpoint(self, x):
577 self.docreate()
578 x_pt, y_pt = self.positioner.vtickpoint_pt(self.axis.convert(self.data, x))
579 return x_pt * unit.t_pt, y_pt * unit.t_pt
581 def vtickdirection(self, v):
582 return self.positioner.vtickdirection(v)
584 def tickdirection(self, x):
585 self.docreate()
586 return self.positioner.vtickdirection(self.axis.convert(self.data, x))
588 def create(self):
589 if self.canvas is None:
590 assert self.positioner is not None, self.errorname
591 self.canvas = self.axis.create(self.data, self.positioner, self.graphtexrunner, self.errorname)
592 return self.canvas
595 class linkedaxis(anchoredaxis):
597 def __init__(self, linkedaxis=None, errorname="manual-linked", painter=_marker):
598 self.painter = painter
599 self.linkedto = None
600 self.errorname = errorname
601 self.canvas = None
602 self.positioner = None
603 if linkedaxis:
604 self.setlinkedaxis(linkedaxis)
606 def setlinkedaxis(self, linkedaxis):
607 assert isinstance(linkedaxis, anchoredaxis), self.errorname
608 self.linkedto = linkedaxis
609 self.axis = linkedaxis.axis
610 self.graphtexrunner = self.linkedto.graphtexrunner
611 self.errorname = "%s (linked to %s)" % (self.errorname, linkedaxis.errorname)
612 self.data = linkedaxis.data
613 if self.painter is _marker:
614 self.painter = linkedaxis.axis.linkpainter
616 def create(self):
617 assert self.linkedto is not None, self.errorname
618 assert self.positioner is not None, self.errorname
619 if self.canvas is None:
620 self.linkedto.docreate()
621 self.canvas = self.axis.createlinked(self.data, self.positioner, self.graphtexrunner, self.errorname, self.painter)
622 return self.canvas
625 class anchoredpathaxis(anchoredaxis):
626 """an anchored axis along a path"""
628 def __init__(self, path, axis, **kwargs):
629 anchoredaxis.__init__(self, axis, text.defaulttexrunner, "pathaxis")
630 self.setpositioner(positioner.pathpositioner(path, **kwargs))
631 self.create()
633 def pathaxis(*args, **kwargs):
634 """creates an axiscanvas for an axis along a path"""
635 return anchoredpathaxis(*args, **kwargs).canvas