make divisor=1 a noop
[PyX.git] / graph / axis / axis.py
blob0b9d915d65b9864550e6854d3f8b30c57bb7eca0
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 math, warnings
25 from pyx import attr, unit, text
26 from pyx.graph.axis import painter, parter, positioner, rater, texter, tick
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 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.mixed(), 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 class variant:
150 def __init__(self, data, **kwargs):
151 self.data = data
152 for key, value in kwargs.items():
153 setattr(self, key, value)
155 def __getattr__(self, key):
156 return getattr(data, key)
158 def __cmp__(self, other):
159 # we can also sort variants by their rate
160 return cmp(self.rate, other.rate)
162 # build a list of variants
163 bestrate = None
164 if self.divisor is not None:
165 if data.min is not None:
166 data_min_divided = data.min/self.divisor
167 else:
168 data_min_divided = None
169 if data.max is not None:
170 data_max_divided = data.max/self.divisor
171 else:
172 data_max_divided = None
173 partfunctions = parter.partfunctions(data_min_divided, data_max_divided,
174 self.min is None, self.max is None)
175 else:
176 partfunctions = parter.partfunctions(data.min, data.max,
177 self.min is None, self.max is None)
178 variants = []
179 for partfunction in partfunctions:
180 worse = 0
181 while worse < self.maxworse:
182 worse += 1
183 ticks = partfunction()
184 if ticks is None:
185 break
186 ticks = tick.mergeticklists(self.manualticks, ticks, mergeequal=0)
187 if ticks:
188 rate = rater.rateticks(self, ticks, self.density)
189 if rate is not None:
190 if self.reverse:
191 rate += rater.raterange(self.convert(data, convert_tick(ticks[0])) -
192 self.convert(data, convert_tick(ticks[-1])), 1)
193 else:
194 rate += rater.raterange(self.convert(data, convert_tick(ticks[-1])) -
195 self.convert(data, convert_tick(ticks[0])), 1)
196 if bestrate is None or rate < bestrate:
197 bestrate = rate
198 worse = 0
199 variants.append(variant(data, rate=rate, ticks=ticks))
201 if not variants:
202 raise RuntimeError("no axis partitioning found%s" % errorname)
204 if len(variants) == 1 or self.painter is None:
205 # When the painter is None, we could sort the variants here by their rating.
206 # However, we didn't did this so far and there is no real reason to change that.
207 data.ticks = variants[0].ticks
208 return layout(data)
210 # build the layout for best variants
211 for variant in variants:
212 variant.storedcanvas = None
213 variants.sort()
214 while not variants[0].storedcanvas:
215 variants[0].storedcanvas = layout(variants[0])
216 ratelayout = rater.ratelayout(variants[0].storedcanvas, self.density)
217 if ratelayout is None:
218 del variants[0]
219 if not variants:
220 raise NoValidPartitionError("no valid axis partitioning found%s" % errorname)
221 else:
222 variants[0].rate += ratelayout
223 variants.sort()
224 self.adjustaxis(data, variants[0].ticks, graphtexrunner, errorname)
225 data.ticks = variants[0].ticks
226 return variants[0].storedcanvas
229 class linear(_regularaxis):
230 """linear axis"""
232 def __init__(self, parter=parter.autolinear(), rater=rater.linear(), **args):
233 _regularaxis.__init__(self, **args)
234 self.parter = parter
235 self.rater = rater
237 def convert(self, data, value):
238 """axis coordinates -> graph coordinates"""
239 if self.reverse:
240 return (data.max - float(value)) / (data.max - data.min)
241 else:
242 return (float(value) - data.min) / (data.max - data.min)
244 def create(self, data, positioner, graphtexrunner, errorname):
245 return _regularaxis._create(self, data, positioner, graphtexrunner, self.parter, self.rater, errorname)
247 lin = linear
250 class logarithmic(_regularaxis):
251 """logarithmic axis"""
253 def __init__(self, parter=parter.autologarithmic(), rater=rater.logarithmic(),
254 linearparter=parter.autolinear(extendtick=None), **args):
255 _regularaxis.__init__(self, **args)
256 self.parter = parter
257 self.rater = rater
258 self.linearparter = linearparter
260 def convert(self, data, value):
261 """axis coordinates -> graph coordinates"""
262 # TODO: store log(data.min) and log(data.max)
263 if self.reverse:
264 return (math.log(data.max) - math.log(float(value))) / (math.log(data.max) - math.log(data.min))
265 else:
266 return (math.log(float(value)) - math.log(data.min)) / (math.log(data.max) - math.log(data.min))
268 def create(self, data, positioner, graphtexrunner, errorname):
269 try:
270 return _regularaxis._create(self, data, positioner, graphtexrunner, self.parter, self.rater, errorname)
271 except NoValidPartitionError:
272 if self.linearparter:
273 warnings.warn("no valid logarithmic partitioning found for axis %s, switch to linear partitioning" % errorname)
274 return _regularaxis._create(self, data, positioner, graphtexrunner, self.linearparter, self.rater, errorname)
275 raise
277 log = logarithmic
280 class subaxispositioner(positioner._positioner):
281 """a subaxis positioner"""
283 def __init__(self, basepositioner, subaxis):
284 self.basepositioner = basepositioner
285 self.vmin = subaxis.vmin
286 self.vmax = subaxis.vmax
287 self.vminover = subaxis.vminover
288 self.vmaxover = subaxis.vmaxover
290 def vbasepath(self, v1=None, v2=None):
291 if v1 is not None:
292 v1 = self.vmin+v1*(self.vmax-self.vmin)
293 else:
294 v1 = self.vminover
295 if v2 is not None:
296 v2 = self.vmin+v2*(self.vmax-self.vmin)
297 else:
298 v2 = self.vmaxover
299 return self.basepositioner.vbasepath(v1, v2)
301 def vgridpath(self, v):
302 return self.basepositioner.vgridpath(self.vmin+v*(self.vmax-self.vmin))
304 def vtickpoint_pt(self, v, axis=None):
305 return self.basepositioner.vtickpoint_pt(self.vmin+v*(self.vmax-self.vmin))
307 def vtickdirection(self, v, axis=None):
308 return self.basepositioner.vtickdirection(self.vmin+v*(self.vmax-self.vmin))
311 class bar(_axis):
313 def __init__(self, subaxes=None, defaultsubaxis=linear(painter=None, linkpainter=None, parter=None),
314 dist=0.5, firstdist=None, lastdist=None, title=None, reverse=0,
315 painter=painter.bar(), linkpainter=painter.linkedbar()):
316 self.subaxes = subaxes
317 self.defaultsubaxis = defaultsubaxis
318 self.dist = dist
319 if firstdist is not None:
320 self.firstdist = firstdist
321 else:
322 self.firstdist = 0.5 * dist
323 if lastdist is not None:
324 self.lastdist = lastdist
325 else:
326 self.lastdist = 0.5 * dist
327 self.title = title
328 self.reverse = reverse
329 self.painter = painter
330 self.linkpainter = linkpainter
332 def createdata(self, errorname):
333 data = axisdata(size=self.firstdist+self.lastdist-self.dist, subaxes={}, names=[])
334 return data
336 def addsubaxis(self, data, name, subaxis, graphtexrunner, errorname):
337 subaxis = anchoredaxis(subaxis, graphtexrunner, "%s, subaxis %s" % (errorname, name))
338 subaxis.setcreatecall(lambda: None)
339 subaxis.sized = hasattr(subaxis.data, "size")
340 if subaxis.sized:
341 data.size += subaxis.data.size
342 else:
343 data.size += 1
344 data.size += self.dist
345 data.subaxes[name] = subaxis
346 if self.reverse:
347 data.names.insert(0, name)
348 else:
349 data.names.append(name)
351 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
352 for value in columndata:
354 # some checks and error messages
355 try:
356 len(value)
357 except:
358 raise ValueError("tuple expected by bar axis '%s'" % errorname)
359 try:
360 value + ""
361 except:
362 pass
363 else:
364 raise ValueError("tuple expected by bar axis '%s'" % errorname)
365 assert len(value) == 2, "tuple of size two expected by bar axis '%s'" % errorname
367 name = value[0]
368 if name is not None and name not in data.names:
369 if self.subaxes:
370 if self.subaxes[name] is not None:
371 self.addsubaxis(data, name, self.subaxes[name], graphtexrunner, errorname)
372 else:
373 self.addsubaxis(data, name, self.defaultsubaxis, graphtexrunner, errorname)
374 for name in data.names:
375 subaxis = data.subaxes[name]
376 if subaxis.sized:
377 data.size -= subaxis.data.size
378 subaxis.axis.adjustaxis(subaxis.data,
379 [value[1] for value in columndata if value[0] == name],
380 graphtexrunner,
381 "%s, subaxis %s" % (errorname, name))
382 if subaxis.sized:
383 data.size += subaxis.data.size
385 def convert(self, data, value):
386 if value[0] is None:
387 return None
388 axis = data.subaxes[value[0]]
389 vmin = axis.vmin
390 vmax = axis.vmax
391 return axis.vmin + axis.convert(value[1]) * (axis.vmax - axis.vmin)
393 def create(self, data, positioner, graphtexrunner, errorname):
394 canvas = painter.axiscanvas(self.painter, graphtexrunner)
395 v = 0
396 position = self.firstdist
397 for name in data.names:
398 subaxis = data.subaxes[name]
399 subaxis.vmin = position / float(data.size)
400 if subaxis.sized:
401 position += subaxis.data.size
402 else:
403 position += 1
404 subaxis.vmax = position / float(data.size)
405 position += 0.5*self.dist
406 subaxis.vminover = v
407 if name == data.names[-1]:
408 subaxis.vmaxover = 1
409 else:
410 subaxis.vmaxover = position / float(data.size)
411 subaxis.setpositioner(subaxispositioner(positioner, subaxis))
412 subaxis.create()
413 for layer, subcanvas in subaxis.canvas.layers.items():
414 canvas.layer(layer).insert(subcanvas)
415 assert len(subaxis.canvas.layers) == len(subaxis.canvas.items)
416 if canvas.extent_pt < subaxis.canvas.extent_pt:
417 canvas.extent_pt = subaxis.canvas.extent_pt
418 position += 0.5*self.dist
419 v = subaxis.vmaxover
420 if self.painter is not None:
421 self.painter.paint(canvas, data, self, positioner)
422 return canvas
424 def createlinked(self, data, positioner, graphtexrunner, errorname, linkpainter):
425 canvas = painter.axiscanvas(self.painter, graphtexrunner)
426 for name in data.names:
427 subaxis = data.subaxes[name]
428 subaxis = linkedaxis(subaxis, name)
429 subaxis.setpositioner(subaxispositioner(positioner, data.subaxes[name]))
430 subaxis.create()
431 for layer, subcanvas in subaxis.canvas.layers.items():
432 canvas.layer(layer).insert(subcanvas)
433 assert len(subaxis.canvas.layers) == len(subaxis.canvas.items)
434 if canvas.extent_pt < subaxis.canvas.extent_pt:
435 canvas.extent_pt = subaxis.canvas.extent_pt
436 if linkpainter is not None:
437 linkpainter.paint(canvas, data, self, positioner)
438 return canvas
441 class nestedbar(bar):
443 def __init__(self, defaultsubaxis=bar(dist=0, painter=None, linkpainter=None), **kwargs):
444 bar.__init__(self, defaultsubaxis=defaultsubaxis, **kwargs)
447 class split(bar):
449 def __init__(self, defaultsubaxis=linear(),
450 firstdist=0, lastdist=0,
451 painter=painter.split(), linkpainter=painter.linkedsplit(), **kwargs):
452 bar.__init__(self, defaultsubaxis=defaultsubaxis,
453 firstdist=firstdist, lastdist=lastdist,
454 painter=painter, linkpainter=linkpainter, **kwargs)
457 class sizedlinear(linear):
459 def __init__(self, size=1, **kwargs):
460 linear.__init__(self, **kwargs)
461 self.size = size
463 def createdata(self, errorname):
464 data = linear.createdata(self, errorname)
465 data.size = self.size
466 return data
468 sizedlin = sizedlinear
471 class autosizedlinear(linear):
473 def __init__(self, parter=parter.autolinear(extendtick=None), **kwargs):
474 linear.__init__(self, parter=parter, **kwargs)
476 def createdata(self, errorname):
477 data = linear.createdata(self, errorname)
478 try:
479 data.size = data.max - data.min
480 except:
481 data.size = 0
482 return data
484 def adjustaxis(self, data, columndata, graphtexrunner, errorname):
485 linear.adjustaxis(self, data, columndata, graphtexrunner, errorname)
486 try:
487 data.size = data.max - data.min
488 except:
489 data.size = 0
491 def create(self, data, positioner, graphtexrunner, errorname):
492 min = data.min
493 max = data.max
494 canvas = linear.create(self, data, positioner, graphtexrunner, errorname)
495 if min != data.min or max != data.max:
496 raise RuntimeError("range change during axis creation of autosized linear axis")
497 return canvas
499 autosizedlin = autosizedlinear
502 class anchoredaxis:
504 def __init__(self, axis, graphtexrunner, errorname):
505 assert not isinstance(axis, anchoredaxis), errorname
506 self.axis = axis
507 self.errorname = errorname
508 self.graphtexrunner = graphtexrunner
509 self.data = axis.createdata(self.errorname)
510 self.canvas = None
511 self.positioner = None
513 def setcreatecall(self, function, *args, **kwargs):
514 self._createfunction = function
515 self._createargs = args
516 self._createkwargs = kwargs
518 def docreate(self):
519 if not self.canvas:
520 self._createfunction(*self._createargs, **self._createkwargs)
522 def setpositioner(self, positioner):
523 assert positioner is not None, self.errorname
524 assert self.positioner is None, self.errorname
525 self.positioner = positioner
527 def convert(self, x):
528 self.docreate()
529 return self.axis.convert(self.data, x)
531 def adjustaxis(self, columndata):
532 if self.canvas is None:
533 self.axis.adjustaxis(self.data, columndata, self.graphtexrunner, self.errorname)
534 else:
535 warnings.warn("ignore axis range adjustment of already created axis '%s'" % self.errorname)
537 def vbasepath(self, v1=None, v2=None):
538 return self.positioner.vbasepath(v1=v1, v2=v2)
540 def basepath(self, x1=None, x2=None):
541 self.docreate()
542 if x1 is None:
543 if x2 is None:
544 return self.positioner.vbasepath()
545 else:
546 return self.positioner.vbasepath(v2=self.axis.convert(self.data, x2))
547 else:
548 if x2 is None:
549 return self.positioner.vbasepath(v1=self.axis.convert(self.data, x1))
550 else:
551 return self.positioner.vbasepath(v1=self.axis.convert(self.data, x1),
552 v2=self.axis.convert(self.data, x2))
554 def vgridpath(self, v):
555 return self.positioner.vgridpath(v)
557 def gridpath(self, x):
558 self.docreate()
559 return self.positioner.vgridpath(self.axis.convert(self.data, x))
561 def vtickpoint_pt(self, v):
562 return self.positioner.vtickpoint_pt(v)
564 def vtickpoint(self, v):
565 return self.positioner.vtickpoint_pt(v) * unit.t_pt
567 def tickpoint_pt(self, x):
568 self.docreate()
569 return self.positioner.vtickpoint_pt(self.axis.convert(self.data, x))
571 def tickpoint(self, x):
572 self.docreate()
573 x_pt, y_pt = self.positioner.vtickpoint_pt(self.axis.convert(self.data, x))
574 return x_pt * unit.t_pt, y_pt * unit.t_pt
576 def vtickdirection(self, v):
577 return self.positioner.vtickdirection(v)
579 def tickdirection(self, x):
580 self.docreate()
581 return self.positioner.vtickdirection(self.axis.convert(self.data, x))
583 def create(self):
584 if self.canvas is None:
585 assert self.positioner is not None, self.errorname
586 self.canvas = self.axis.create(self.data, self.positioner, self.graphtexrunner, self.errorname)
587 return self.canvas
590 class linkedaxis(anchoredaxis):
592 def __init__(self, linkedaxis=None, errorname="manual-linked", painter=_marker):
593 self.painter = painter
594 self.linkedto = None
595 self.errorname = errorname
596 self.canvas = None
597 self.positioner = None
598 if linkedaxis:
599 self.setlinkedaxis(linkedaxis)
601 def setlinkedaxis(self, linkedaxis):
602 assert isinstance(linkedaxis, anchoredaxis), self.errorname
603 self.linkedto = linkedaxis
604 self.axis = linkedaxis.axis
605 self.graphtexrunner = self.linkedto.graphtexrunner
606 self.errorname = "%s (linked to %s)" % (self.errorname, linkedaxis.errorname)
607 self.data = linkedaxis.data
608 if self.painter is _marker:
609 self.painter = linkedaxis.axis.linkpainter
611 def create(self):
612 assert self.linkedto is not None, self.errorname
613 assert self.positioner is not None, self.errorname
614 if self.canvas is None:
615 self.linkedto.docreate()
616 self.canvas = self.axis.createlinked(self.data, self.positioner, self.graphtexrunner, self.errorname, self.painter)
617 return self.canvas
620 class anchoredpathaxis(anchoredaxis):
621 """an anchored axis along a path"""
623 def __init__(self, path, axis, **kwargs):
624 anchoredaxis.__init__(self, axis, text.defaulttexrunner, "pathaxis")
625 self.setpositioner(positioner.pathpositioner(path, **kwargs))
626 self.create()
628 def pathaxis(*args, **kwargs):
629 """creates an axiscanvas for an axis along a path"""
630 return anchoredpathaxis(*args, **kwargs).canvas