properly rotate axis titles when ticks are not othorgonal to the axis (reported by...
[PyX/mjg.git] / pyx / graph / axis / painter.py
blobdbc4ccc5fdd947c494016ce1af5d9aba98a4f767
1 # -*- coding: ISO-8859-1 -*-
4 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2004 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
25 import math
26 from pyx import canvas, color, attr, text, style, unit, box, path
27 from pyx import trafo as trafomodule
28 from pyx.graph.axis import tick
31 goldenmean = 0.5 * (math.sqrt(5) + 1)
34 class axiscanvas(canvas.canvas):
35 """axis canvas"""
37 def __init__(self, painter, graphtexrunner):
38 """initializes the instance
39 - sets extent to zero
40 - sets labels to an empty list"""
41 canvas._canvas.__init__(self)
42 self.extent_pt = 0
43 self.labels = []
44 if isinstance(painter, _text) and painter.texrunner:
45 self.settexrunner(painter.texrunner)
46 else:
47 self.settexrunner(graphtexrunner)
50 class rotatetext:
51 """create rotations accordingly to tick directions"""
53 def __init__(self, direction, epsilon=1e-10):
54 self.direction = direction
55 self.epsilon = epsilon
57 def trafo(self, dx, dy):
58 direction = self.direction + math.atan2(dy, dx) * 180 / math.pi
59 while (direction > 180 + self.epsilon):
60 direction -= 360
61 while (direction < -180 - self.epsilon):
62 direction += 360
63 while (direction > 90 + self.epsilon):
64 direction -= 180
65 while (direction < -90 - self.epsilon):
66 direction += 180
67 return trafomodule.rotate(direction)
70 rotatetext.parallel = rotatetext(90)
71 rotatetext.orthogonal = rotatetext(180)
74 class _text:
75 """a painter with a texrunner"""
77 def __init__(self, texrunner=None):
78 self.texrunner = texrunner
81 class _title(_text):
82 """class for painting an axis title"""
84 defaulttitleattrs = [text.halign.center, text.vshift.mathaxis]
86 def __init__(self, titledist=0.3*unit.v_cm,
87 titleattrs=[],
88 titledirection=rotatetext.parallel,
89 titlepos=0.5,
90 **kwargs):
91 self.titledist = titledist
92 self.titleattrs = titleattrs
93 self.titledirection = titledirection
94 self.titlepos = titlepos
95 _text.__init__(self, **kwargs)
97 def paint(self, canvas, data, axis, axispos):
98 if axis.title is not None and self.titleattrs is not None:
99 x, y = axispos.vtickpoint_pt(self.titlepos)
100 dx, dy = axispos.vtickdirection(self.titlepos)
101 titleattrs = self.defaulttitleattrs + self.titleattrs
102 if self.titledirection is not None:
103 x2, y2 = axispos.vtickpoint_pt(self.titlepos+0.001) # XXX: axisdirection needed
104 dx2, dy2 = x2-x, y2-y
105 if dx*dy2-dy*dx2 < 0:
106 dy2 *= -1
107 dx2 *= -1
108 titleattrs.append(self.titledirection.trafo(dy2, -dx2))
109 title = canvas.text_pt(x, y, axis.title, titleattrs)
110 canvas.extent_pt += unit.topt(self.titledist)
111 title.linealign_pt(canvas.extent_pt, -dx, -dy)
112 canvas.extent_pt += title.extent_pt(dx, dy)
115 class geometricseries(attr.changeattr):
117 def __init__(self, initial, factor):
118 self.initial = initial
119 self.factor = factor
121 def select(self, index, total):
122 return self.initial * (self.factor ** index)
125 class ticklength(geometricseries): pass
127 _base = 0.12 * unit.v_cm
129 ticklength.SHORT = ticklength(_base/math.sqrt(64), 1/goldenmean)
130 ticklength.SHORt = ticklength(_base/math.sqrt(32), 1/goldenmean)
131 ticklength.SHOrt = ticklength(_base/math.sqrt(16), 1/goldenmean)
132 ticklength.SHort = ticklength(_base/math.sqrt(8), 1/goldenmean)
133 ticklength.Short = ticklength(_base/math.sqrt(4), 1/goldenmean)
134 ticklength.short = ticklength(_base/math.sqrt(2), 1/goldenmean)
135 ticklength.normal = ticklength(_base, 1/goldenmean)
136 ticklength.long = ticklength(_base*math.sqrt(2), 1/goldenmean)
137 ticklength.Long = ticklength(_base*math.sqrt(4), 1/goldenmean)
138 ticklength.LOng = ticklength(_base*math.sqrt(8), 1/goldenmean)
139 ticklength.LONg = ticklength(_base*math.sqrt(16), 1/goldenmean)
140 ticklength.LONG = ticklength(_base*math.sqrt(32), 1/goldenmean)
143 class regular(_title):
144 """class for painting the ticks and labels of an axis"""
146 defaulttickattrs = []
147 defaultgridattrs = []
148 defaultbasepathattrs = [style.linecap.square]
149 defaultlabelattrs = [text.halign.center, text.vshift.mathaxis]
151 def __init__(self, innerticklength=ticklength.normal,
152 outerticklength=None,
153 tickattrs=[],
154 gridattrs=None,
155 basepathattrs=[],
156 labeldist=0.3*unit.v_cm,
157 labelattrs=[],
158 labeldirection=None,
159 labelhequalize=0,
160 labelvequalize=1,
161 **kwargs):
162 self.innerticklength = innerticklength
163 self.outerticklength = outerticklength
164 self.tickattrs = tickattrs
165 self.gridattrs = gridattrs
166 self.basepathattrs = basepathattrs
167 self.labeldist = labeldist
168 self.labelattrs = labelattrs
169 self.labeldirection = labeldirection
170 self.labelhequalize = labelhequalize
171 self.labelvequalize = labelvequalize
172 _title.__init__(self, **kwargs)
174 def paint(self, canvas, data, axis, axispos):
175 for t in data.ticks:
176 t.temp_v = axis.convert(data, t)
177 t.temp_x_pt, t.temp_y_pt = axispos.vtickpoint_pt(t.temp_v)
178 t.temp_dx, t.temp_dy = axispos.vtickdirection(t.temp_v)
179 maxticklevel, maxlabellevel = tick.maxlevels(data.ticks)
180 labeldist_pt = unit.topt(self.labeldist)
182 # create & align t.temp_labelbox
183 for t in data.ticks:
184 if t.labellevel is not None:
185 labelattrs = attr.selectattrs(self.labelattrs, t.labellevel, maxlabellevel)
186 if labelattrs is not None:
187 labelattrs = self.defaultlabelattrs + labelattrs
188 if self.labeldirection is not None:
189 labelattrs.append(self.labeldirection.trafo(t.temp_dx, t.temp_dy))
190 if t.labelattrs is not None:
191 labelattrs.extend(t.labelattrs)
192 t.temp_labelbox = canvas.texrunner.text_pt(t.temp_x_pt, t.temp_y_pt, t.label, labelattrs)
193 if len(data.ticks) > 1:
194 equaldirection = 1
195 for t in data.ticks[1:]:
196 if t.temp_dx != data.ticks[0].temp_dx or t.temp_dy != data.ticks[0].temp_dy:
197 equaldirection = 0
198 else:
199 equaldirection = 0
200 if equaldirection and ((not data.ticks[0].temp_dx and self.labelvequalize) or
201 (not data.ticks[0].temp_dy and self.labelhequalize)):
202 if self.labelattrs is not None:
203 box.linealignequal_pt([t.temp_labelbox for t in data.ticks if t.labellevel is not None],
204 labeldist_pt, -data.ticks[0].temp_dx, -data.ticks[0].temp_dy)
205 else:
206 for t in data.ticks:
207 if t.labellevel is not None and self.labelattrs is not None:
208 t.temp_labelbox.linealign_pt(labeldist_pt, -t.temp_dx, -t.temp_dy)
210 for t in data.ticks:
211 if t.ticklevel is not None and self.tickattrs is not None:
212 tickattrs = attr.selectattrs(self.defaulttickattrs + self.tickattrs, t.ticklevel, maxticklevel)
213 if tickattrs is not None:
214 innerticklength = attr.selectattr(self.innerticklength, t.ticklevel, maxticklevel)
215 outerticklength = attr.selectattr(self.outerticklength, t.ticklevel, maxticklevel)
216 if innerticklength is not None or outerticklength is not None:
217 if innerticklength is None:
218 innerticklength = 0
219 if outerticklength is None:
220 outerticklength = 0
221 innerticklength_pt = unit.topt(innerticklength)
222 outerticklength_pt = unit.topt(outerticklength)
223 x1 = t.temp_x_pt + t.temp_dx * innerticklength_pt
224 y1 = t.temp_y_pt + t.temp_dy * innerticklength_pt
225 x2 = t.temp_x_pt - t.temp_dx * outerticklength_pt
226 y2 = t.temp_y_pt - t.temp_dy * outerticklength_pt
227 canvas.stroke(path.line_pt(x1, y1, x2, y2), tickattrs)
228 if outerticklength_pt > canvas.extent_pt:
229 canvas.extent_pt = outerticklength_pt
230 if -innerticklength_pt > canvas.extent_pt:
231 canvas.extent_pt = -innerticklength_pt
232 if self.gridattrs is not None:
233 gridattrs = attr.selectattrs(self.defaultgridattrs + self.gridattrs, t.ticklevel, maxticklevel)
234 if gridattrs is not None:
235 canvas.stroke(axispos.vgridpath(t.temp_v), gridattrs)
236 if t.labellevel is not None and self.labelattrs is not None:
237 canvas.insert(t.temp_labelbox)
238 canvas.labels.append(t.temp_labelbox)
239 extent_pt = t.temp_labelbox.extent_pt(t.temp_dx, t.temp_dy) + labeldist_pt
240 if extent_pt > canvas.extent_pt:
241 canvas.extent_pt = extent_pt
243 if self.labelattrs is None:
244 canvas.labels = None
246 if self.basepathattrs is not None:
247 canvas.stroke(axispos.vbasepath(), self.defaultbasepathattrs + self.basepathattrs)
249 # for t in data.ticks:
250 # del t.temp_v # we've inserted those temporary variables ... and do not care any longer about them
251 # del t.temp_x_pt
252 # del t.temp_y_pt
253 # del t.temp_dx
254 # del t.temp_dy
255 # if t.labellevel is not None and self.labelattrs is not None:
256 # del t.temp_labelbox
258 _title.paint(self, canvas, data, axis, axispos)
261 class linked(regular):
262 """class for painting a linked axis"""
264 def __init__(self, labelattrs=None, # turn off labels and title
265 titleattrs=None,
266 **kwargs):
267 regular.__init__(self, labelattrs=labelattrs,
268 titleattrs=titleattrs,
269 **kwargs)
272 class bar(_title):
273 """class for painting a baraxis"""
275 defaulttickattrs = []
276 defaultbasepathattrs = [style.linecap.square]
277 defaultnameattrs = [text.halign.center, text.vshift.mathaxis]
279 def __init__(self, innerticklength=None,
280 outerticklength=None,
281 tickattrs=[],
282 basepathattrs=[],
283 namedist=0.3*unit.v_cm,
284 nameattrs=[],
285 namedirection=None,
286 namepos=0.5,
287 namehequalize=0,
288 namevequalize=1,
289 **args):
290 self.innerticklength = innerticklength
291 self.outerticklength = outerticklength
292 self.tickattrs = tickattrs
293 self.basepathattrs = basepathattrs
294 self.namedist = namedist
295 self.nameattrs = nameattrs
296 self.namedirection = namedirection
297 self.namepos = namepos
298 self.namehequalize = namehequalize
299 self.namevequalize = namevequalize
300 _title.__init__(self, **args)
302 def paint(self, canvas, data, axis, positioner):
303 namepos = []
304 for name in data.names:
305 subaxis = data.subaxes[name]
306 v = subaxis.vmin + self.namepos * (subaxis.vmax - subaxis.vmin)
307 x, y = positioner.vtickpoint_pt(v)
308 dx, dy = positioner.vtickdirection(v)
309 namepos.append((v, x, y, dx, dy))
310 nameboxes = []
311 if self.nameattrs is not None:
312 for (v, x, y, dx, dy), name in zip(namepos, data.names):
313 nameattrs = self.defaultnameattrs + self.nameattrs
314 if self.namedirection is not None:
315 nameattrs.append(self.namedirection.trafo(tick.temp_dx, tick.temp_dy))
316 nameboxes.append(canvas.texrunner.text_pt(x, y, str(name), nameattrs))
317 labeldist_pt = canvas.extent_pt + unit.topt(self.namedist)
318 if len(namepos) > 1:
319 equaldirection = 1
320 for np in namepos[1:]:
321 if np[3] != namepos[0][3] or np[4] != namepos[0][4]:
322 equaldirection = 0
323 else:
324 equaldirection = 0
325 if equaldirection and ((not namepos[0][3] and self.namevequalize) or
326 (not namepos[0][4] and self.namehequalize)):
327 box.linealignequal_pt(nameboxes, labeldist_pt, -namepos[0][3], -namepos[0][4])
328 else:
329 for namebox, np in zip(nameboxes, namepos):
330 namebox.linealign_pt(labeldist_pt, -np[3], -np[4])
331 if self.basepathattrs is not None:
332 p = positioner.vbasepath()
333 if p is not None:
334 canvas.stroke(p, self.defaultbasepathattrs + self.basepathattrs)
335 if ( self.tickattrs is not None and
336 (self.innerticklength is not None or self.outerticklength is not None) ):
337 if self.innerticklength is not None:
338 innerticklength_pt = unit.topt(self.innerticklength)
339 if canvas.extent_pt < -innerticklength_pt:
340 canvas.extent_pt = -innerticklength_pt
341 elif self.outerticklength is not None:
342 innerticklength_pt = 0
343 if self.outerticklength is not None:
344 outerticklength_pt = unit.topt(self.outerticklength)
345 if canvas.extent_pt < outerticklength_pt:
346 canvas.extent_pt = outerticklength_pt
347 elif innerticklength_pt is not None:
348 outerticklength_pt = 0
349 for v in [data.subaxes[name].vminover for name in data.names] + [1]:
350 x, y = positioner.vtickpoint_pt(v)
351 dx, dy = positioner.vtickdirection(v)
352 x1 = x + dx * innerticklength_pt
353 y1 = y + dy * innerticklength_pt
354 x2 = x - dx * outerticklength_pt
355 y2 = y - dy * outerticklength_pt
356 canvas.stroke(path.line_pt(x1, y1, x2, y2), self.defaulttickattrs + self.tickattrs)
357 for (v, x, y, dx, dy), namebox in zip(namepos, nameboxes):
358 newextent_pt = namebox.extent_pt(dx, dy) + labeldist_pt
359 if canvas.extent_pt < newextent_pt:
360 canvas.extent_pt = newextent_pt
361 for namebox in nameboxes:
362 canvas.insert(namebox)
363 _title.paint(self, canvas, data, axis, positioner)
366 class linkedbar(bar):
367 """class for painting a linked baraxis"""
369 def __init__(self, nameattrs=None, titleattrs=None, **kwargs):
370 bar.__init__(self, nameattrs=nameattrs, titleattrs=titleattrs, **kwargs)
372 def getsubaxis(self, subaxis, name):
373 from pyx.graph.axis import linkedaxis
374 return linkedaxis(subaxis, name)
377 class split(_title):
378 """class for painting a splitaxis"""
380 defaultbreaklinesattrs = []
382 def __init__(self, breaklinesdist=0.05*unit.v_cm,
383 breaklineslength=0.5*unit.v_cm,
384 breaklinesangle=-60,
385 breaklinesattrs=[],
386 **args):
387 self.breaklinesdist = breaklinesdist
388 self.breaklineslength = breaklineslength
389 self.breaklinesangle = breaklinesangle
390 self.breaklinesattrs = breaklinesattrs
391 self.sin = math.sin(self.breaklinesangle*math.pi/180.0)
392 self.cos = math.cos(self.breaklinesangle*math.pi/180.0)
393 _title.__init__(self, **args)
395 def paint(self, canvas, data, axis, axispos):
396 if self.breaklinesattrs is not None:
397 breaklinesdist_pt = unit.topt(self.breaklinesdist)
398 breaklineslength_pt = unit.topt(self.breaklineslength)
399 breaklinesextent_pt = (0.5*breaklinesdist_pt*math.fabs(self.cos) +
400 0.5*breaklineslength_pt*math.fabs(self.sin))
401 if canvas.extent_pt < breaklinesextent_pt:
402 canvas.extent_pt = breaklinesextent_pt
403 for v in [data.subaxes[name].vminover for name in data.names[1:]]:
404 # use a tangent of the basepath (this is independent of the tickdirection)
405 p = axispos.vbasepath(v, None).normpath()
406 breakline = p.tangent(0, length=self.breaklineslength)
407 widthline = p.tangent(0, length=self.breaklinesdist).transformed(trafomodule.rotate(self.breaklinesangle+90, *breakline.atbegin()))
408 # XXX Uiiii
409 tocenter = map(lambda x: 0.5*(x[0]-x[1]), zip(breakline.atbegin(), breakline.atend()))
410 towidth = map(lambda x: 0.5*(x[0]-x[1]), zip(widthline.atbegin(), widthline.atend()))
411 breakline = breakline.transformed(trafomodule.translate(*tocenter).rotated(self.breaklinesangle, *breakline.atbegin()))
412 breakline1 = breakline.transformed(trafomodule.translate(*towidth))
413 breakline2 = breakline.transformed(trafomodule.translate(-towidth[0], -towidth[1]))
414 canvas.fill(path.path(path.moveto_pt(*breakline1.atbegin_pt()),
415 path.lineto_pt(*breakline1.atend_pt()),
416 path.lineto_pt(*breakline2.atend_pt()),
417 path.lineto_pt(*breakline2.atbegin_pt()),
418 path.closepath()), [color.gray.white])
419 canvas.stroke(breakline1, self.defaultbreaklinesattrs + self.breaklinesattrs)
420 canvas.stroke(breakline2, self.defaultbreaklinesattrs + self.breaklinesattrs)
421 _title.paint(self, canvas, data, axis, axispos)
424 class linkedsplit(split):
426 def __init__(self, titleattrs=None, **kwargs):
427 split.__init__(self, titleattrs=titleattrs, **kwargs)