fix insertion of a zero length path
[PyX/mjg.git] / pyx / graph / style.py
blob98b65a458c90c7d35d122e736e56d6f655058488
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-2004 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
26 import re, math
27 from pyx import attr, deco, style, color, unit, canvas, path
28 from pyx import text as textmodule
31 class _style:
33 def setdatapattern(self, graph, columns, pattern):
34 for datakey in columns.keys():
35 match = pattern.match(datakey)
36 if match:
37 # XXX match.groups()[0] must contain the full axisname
38 axisname = match.groups()[0]
39 index = columns[datakey]
40 del columns[datakey]
41 return graph.axes[axisname], index
43 def key_pt(self, c, x_pt, y_pt, width_pt, height_pt, styledata):
44 raise RuntimeError("style doesn't provide a key")
46 def adjustaxes(self, points, columns, styledata):
47 return
49 def setdata(self, graph, columns, styledata):
50 return columns
52 def selectstyle(self, selectindex, selecttotal, styledata):
53 pass
55 def initdrawpoints(self, graph, styledata):
56 pass
58 def drawpoint(self, graph, styledata):
59 pass
61 def donedrawpoints(self, graph, styledata):
62 pass
65 class pointpos(_style):
67 def __init__(self, epsilon=1e-10):
68 self.epsilon = epsilon
70 def setdata(self, graph, columns, styledata):
71 # analyse column information
72 styledata.pointposaxisindex = []
73 columns = columns.copy()
74 for axisname in graph.axisnames:
75 pattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % axisname)
76 styledata.pointposaxisindex.append(self.setdatapattern(graph, columns, pattern)) # TODO: append "needed=1"
77 return columns
79 def adjustaxes(self, points, columns, styledata):
80 for axis, index in styledata.pointposaxisindex:
81 axis.adjustrange(points, index)
83 def drawpoint(self, graph, styledata):
84 # calculate vpos
85 styledata.validvpos = 1 # valid position (but might be outside of the graph)
86 styledata.drawsymbol = 1 # valid position inside the graph
87 styledata.vpos = [axis.convert(styledata.point[index]) for axis, index in styledata.pointposaxisindex]
88 # for axisname in graph.axisnames:
89 # try:
90 # v = styledata.axes[axisname].convert(styledata.point[styledata.index[axisname]["x"]])
91 # except (ArithmeticError, KeyError, ValueError, TypeError):
92 # styledata.validvpos = 0
93 # styledata.drawsymbol = 0
94 # styledata.vpos.append(None)
95 # else:
96 # if v < - self.epsilon or v > 1 + self.epsilon:
97 # styledata.drawsymbol = 0
98 # styledata.vpos.append(v)
101 class rangepos(_style):
103 def setdata(self, graph, columns, styledata):
105 - the instance should be considered read-only
106 (it might be shared between several data)
107 - styledata is the place where to store information
108 - returns the dictionary of columns not used by the style"""
110 # analyse column information
111 styledata.index = {} # a nested index dictionary containing
112 # column numbers, e.g. styledata.index["x"]["x"],
113 # styledata.index["y"]["dmin"] etc.; the first key is a axis
114 # name (without the axis number), the second is one of
115 # the datanames ["x", "min", "max", "d", "dmin", "dmax"]
116 styledata.axes = {} # mapping from axis name (without axis number) to the axis
118 columns = columns.copy()
119 for axisname in graph.axisnames:
120 for dataname, pattern in [("x", re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % axisname)),
121 ("min", re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % axisname)),
122 ("max", re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % axisname)),
123 ("d", re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % axisname)),
124 ("dmin", re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % axisname)),
125 ("dmax", re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % axisname))]:
126 matchresult = self.setdatapattern(graph, columns, pattern)
127 if matchresult is not None:
128 axis, index = matchresult
129 if styledata.axes.has_key(axisname):
130 if styledata.axes[axisname] != axis:
131 raise ValueError("axis mismatch for axis name '%s'" % axisname)
132 styledata.index[axisname][dataname] = index
133 else:
134 styledata.index[axisname] = {dataname: index}
135 styledata.axes[axisname] = axis
136 if not styledata.axes.has_key(axisname):
137 raise ValueError("missing columns for axis name '%s'" % axisname)
138 if ((styledata.index[axisname].has_key("min") and styledata.index[axisname].has_key("d")) or
139 (styledata.index[axisname].has_key("min") and styledata.index[axisname].has_key("dmin")) or
140 (styledata.index[axisname].has_key("d") and styledata.index[axisname].has_key("dmin")) or
141 (styledata.index[axisname].has_key("max") and styledata.index[axisname].has_key("d")) or
142 (styledata.index[axisname].has_key("max") and styledata.index[axisname].has_key("dmax")) or
143 (styledata.index[axisname].has_key("d") and styledata.index[axisname].has_key("dmax"))):
144 raise ValueError("multiple errorbar definition for axis name '%s'" % axisname)
145 if (not styledata.index[axisname].has_key("x") and
146 (styledata.index[axisname].has_key("d") or
147 styledata.index[axisname].has_key("dmin") or
148 styledata.index[axisname].has_key("dmax"))):
149 raise ValueError("errorbar definition start value missing for axis name '%s'" % axisname)
150 return columns
152 def adjustaxes(self, points, columns, styledata):
153 # reverse lookup for axisnames
154 # TODO: the reverse lookup is ugly
155 axisnames = []
156 for column in columns:
157 for axisname in styledata.index.keys():
158 for thiscolumn in styledata.index[axisname].values():
159 if thiscolumn == column and axisname not in axisnames:
160 axisnames.append(axisname)
161 # TODO: perform check to verify that all columns for a given axisname are available at the same time
162 for axisname in axisnames:
163 if styledata.index[axisname].has_key("x"):
164 styledata.axes[axisname].adjustrange(points, styledata.index[axisname]["x"])
165 if styledata.index[axisname].has_key("min"):
166 styledata.axes[axisname].adjustrange(points, styledata.index[axisname]["min"])
167 if styledata.index[axisname].has_key("max"):
168 styledata.axes[axisname].adjustrange(points, styledata.index[axisname]["max"])
169 if styledata.index[axisname].has_key("d"):
170 styledata.axes[axisname].adjustrange(points, styledata.index[axisname]["x"], deltaindex=styledata.index[axisname]["d"])
171 if styledata.index[axisname].has_key("dmin"):
172 styledata.axes[axisname].adjustrange(points, styledata.index[axisname]["x"], deltaminindex=styledata.index[axisname]["dmin"])
173 if styledata.index[axisname].has_key("dmax"):
174 styledata.axes[axisname].adjustrange(points, styledata.index[axisname]["x"], deltamaxindex=styledata.index[axisname]["dmax"])
176 def doerrorbars(self, styledata):
177 # errorbar loop over the different direction having errorbars
178 for erroraxisname, erroraxisindex in styledata.errorlist:
180 # check for validity of other point components
181 i = 0
182 for v in styledata.vpos:
183 if v is None and i != erroraxisindex:
184 break
185 i += 1
186 else:
187 # calculate min and max
188 errorindex = styledata.index[erroraxisname]
189 try:
190 min = styledata.point[errorindex["x"]] - styledata.point[errorindex["d"]]
191 except:
192 try:
193 min = styledata.point[errorindex["x"]] - styledata.point[errorindex["dmin"]]
194 except:
195 try:
196 min = styledata.point[errorindex["min"]]
197 except:
198 min = None
199 try:
200 max = styledata.point[errorindex["x"]] + styledata.point[errorindex["d"]]
201 except:
202 try:
203 max = styledata.point[errorindex["x"]] + styledata.point[errorindex["dmax"]]
204 except:
205 try:
206 max = styledata.point[errorindex["max"]]
207 except:
208 max = None
210 # calculate vmin and vmax
211 try:
212 vmin = styledata.axes[erroraxisname].convert(min)
213 except:
214 vmin = None
215 try:
216 vmax = styledata.axes[erroraxisname].convert(max)
217 except:
218 vmax = None
220 # create vminpos and vmaxpos
221 vcaps = []
222 if vmin is not None:
223 vminpos = styledata.vpos[:]
224 if vmin > - self.epsilon and vmin < 1 + self.epsilon:
225 vminpos[erroraxisindex] = vmin
226 vcaps.append(vminpos)
227 else:
228 vminpos[erroraxisindex] = 0
229 elif styledata.vpos[erroraxisindex] is not None:
230 vminpos = styledata.vpos
231 else:
232 break
233 if vmax is not None:
234 vmaxpos = styledata.vpos[:]
235 if vmax > - self.epsilon and vmax < 1 + self.epsilon:
236 vmaxpos[erroraxisindex] = vmax
237 vcaps.append(vmaxpos)
238 else:
239 vmaxpos[erroraxisindex] = 1
240 elif styledata.vpos[erroraxisindex] is not None:
241 vmaxpos = styledata.vpos
242 else:
243 break
246 class symbol(_style):
248 def cross(self, x_pt, y_pt, size_pt):
249 return (path.moveto_pt(x_pt-0.5*size_pt, y_pt-0.5*size_pt),
250 path.lineto_pt(x_pt+0.5*size_pt, y_pt+0.5*size_pt),
251 path.moveto_pt(x_pt-0.5*size_pt, y_pt+0.5*size_pt),
252 path.lineto_pt(x_pt+0.5*size_pt, y_pt-0.5*size_pt))
254 def plus(self, x_pt, y_pt, size_pt):
255 return (path.moveto_pt(x_pt-0.707106781*size_pt, y_pt),
256 path.lineto_pt(x_pt+0.707106781*size_pt, y_pt),
257 path.moveto_pt(x_pt, y_pt-0.707106781*size_pt),
258 path.lineto_pt(x_pt, y_pt+0.707106781*size_pt))
260 def square(self, x_pt, y_pt, size_pt):
261 return (path.moveto_pt(x_pt-0.5*size_pt, y_pt-0.5*size_pt),
262 path.lineto_pt(x_pt+0.5*size_pt, y_pt-0.5*size_pt),
263 path.lineto_pt(x_pt+0.5*size_pt, y_pt+0.5*size_pt),
264 path.lineto_pt(x_pt-0.5*size_pt, y_pt+0.5*size_pt),
265 path.closepath())
267 def triangle(self, x_pt, y_pt, size_pt):
268 return (path.moveto_pt(x_pt-0.759835685*size_pt, y_pt-0.438691337*size_pt),
269 path.lineto_pt(x_pt+0.759835685*size_pt, y_pt-0.438691337*size_pt),
270 path.lineto_pt(x_pt, y_pt+0.877382675*size_pt),
271 path.closepath())
273 def circle(self, x_pt, y_pt, size_pt):
274 return (path.arc_pt(x_pt, y_pt, 0.564189583*size_pt, 0, 360),
275 path.closepath())
277 def diamond(self, x_pt, y_pt, size_pt):
278 return (path.moveto_pt(x_pt-0.537284965*size_pt, y_pt),
279 path.lineto_pt(x_pt, y_pt-0.930604859*size_pt),
280 path.lineto_pt(x_pt+0.537284965*size_pt, y_pt),
281 path.lineto_pt(x_pt, y_pt+0.930604859*size_pt),
282 path.closepath())
284 changecross = attr.changelist([cross, plus, square, triangle, circle, diamond])
285 changeplus = attr.changelist([plus, square, triangle, circle, diamond, cross])
286 changesquare = attr.changelist([square, triangle, circle, diamond, cross, plus])
287 changetriangle = attr.changelist([triangle, circle, diamond, cross, plus, square])
288 changecircle = attr.changelist([circle, diamond, cross, plus, square, triangle])
289 changediamond = attr.changelist([diamond, cross, plus, square, triangle, circle])
290 changesquaretwice = attr.changelist([square, square, triangle, triangle, circle, circle, diamond, diamond])
291 changetriangletwice = attr.changelist([triangle, triangle, circle, circle, diamond, diamond, square, square])
292 changecircletwice = attr.changelist([circle, circle, diamond, diamond, square, square, triangle, triangle])
293 changediamondtwice = attr.changelist([diamond, diamond, square, square, triangle, triangle, circle, circle])
295 changestrokedfilled = attr.changelist([deco.stroked, deco.filled])
296 changefilledstroked = attr.changelist([deco.filled, deco.stroked])
298 defaultsymbolattrs = [deco.stroked]
300 def __init__(self, symbol=changecross,
301 size=0.2*unit.v_cm,
302 symbolattrs=[],
303 epsilon=1e-10):
304 self.symbol = symbol
305 self.size = size
306 self.symbolattrs = symbolattrs
307 self.epsilon = epsilon
309 def selectstyle(self, selectindex, selecttotal, styledata):
310 styledata.symbol = attr.selectattr(self.symbol, selectindex, selecttotal)
311 styledata.size_pt = unit.topt(attr.selectattr(self.size, selectindex, selecttotal))
312 if self.symbolattrs is not None:
313 styledata.symbolattrs = attr.selectattrs(self.defaultsymbolattrs + self.symbolattrs, selectindex, selecttotal)
314 else:
315 styledata.symbolattrs = None
317 def drawsymbol_pt(self, c, x_pt, y_pt, styledata, point=None):
318 if styledata.symbolattrs is not None:
319 c.draw(path.path(*styledata.symbol(self, x_pt, y_pt, styledata.size_pt)), styledata.symbolattrs)
321 def drawpoint(self, graph, styledata):
322 if styledata.drawsymbol:
323 styledata.xpos, styledata.ypos = graph.vpos_pt(*styledata.vpos)
324 self.drawsymbol_pt(graph, styledata.xpos, styledata.ypos, styledata, point=styledata.point)
326 def key_pt(self, c, x_pt, y_pt, width_pt, height_pt, styledata):
327 self.drawsymbol_pt(c, x_pt+0.5*width_pt, y_pt+0.5*height_pt, styledata)
328 if styledata.lineattrs is not None:
329 c.stroke(path.line_pt(x_pt, y_pt+0.5*height_pt, x_pt+width_pt, y_pt+0.5*height_pt), styledata.lineattrs)
332 class line(_style):
334 changelinestyle = attr.changelist([style.linestyle.solid,
335 style.linestyle.dashed,
336 style.linestyle.dotted,
337 style.linestyle.dashdotted])
339 defaultlineattrs = [changelinestyle]
341 def __init__(self, lineattrs=[]):
342 self.lineattrs = lineattrs
344 def selectstyle(self, selectindex, selecttotal, styledata):
345 styledata.lineattrs = attr.selectattrs(self.defaultlineattrs + self.lineattrs, selectindex, selecttotal)
347 def initdrawpoints(self, graph, styledata):
348 styledata.linecanvas = graph.insert(canvas.canvas())
349 styledata.path = path.path()
350 styledata.linebasepoints = []
351 styledata.lastvpos = None
353 def appendlinebasepoints(self, graph, styledata):
354 # append linebasepoints
355 if styledata.validvpos:
356 if len(styledata.linebasepoints):
357 # the last point was inside the graph
358 if styledata.drawsymbol: # this is wrong
359 # we always need the following line here:
360 styledata.xpos, styledata.ypos = graph.vpos_pt(*styledata.vpos)
361 styledata.linebasepoints.append((styledata.xpos, styledata.ypos))
362 else:
363 # cut end
364 cut = 1
365 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
366 newcut = None
367 if vend > 1:
368 # 1 = vstart + (vend - vstart) * cut
369 try:
370 newcut = (1 - vstart)/(vend - vstart)
371 except ArithmeticError:
372 break
373 if vend < 0:
374 # 0 = vstart + (vend - vstart) * cut
375 try:
376 newcut = - vstart/(vend - vstart)
377 except ArithmeticError:
378 break
379 if newcut is not None and newcut < cut:
380 cut = newcut
381 else:
382 cutvpos = []
383 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
384 cutvpos.append(vstart + (vend - vstart) * cut)
385 styledata.linebasepoints.append(styledata.graph.vpos_pt(*cutvpos))
386 styledata.validvpos = 0 # clear linebasepoints below
387 else:
388 # the last point was outside the graph
389 if styledata.lastvpos is not None:
390 if styledata.drawsymbol:
391 # cut beginning
392 cut = 0
393 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
394 newcut = None
395 if vstart > 1:
396 # 1 = vstart + (vend - vstart) * cut
397 try:
398 newcut = (1 - vstart)/(vend - vstart)
399 except ArithmeticError:
400 break
401 if vstart < 0:
402 # 0 = vstart + (vend - vstart) * cut
403 try:
404 newcut = - vstart/(vend - vstart)
405 except ArithmeticError:
406 break
407 if newcut is not None and newcut > cut:
408 cut = newcut
409 else:
410 cutvpos = []
411 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
412 cutvpos.append(vstart + (vend - vstart) * cut)
413 styledata.linebasepoints.append(graph.vpos_pt(*cutvpos))
414 styledata.linebasepoints.append(graph.vpos_pt(*styledata.vpos))
415 else:
416 # sometimes cut beginning and end
417 cutfrom = 0
418 cutto = 1
419 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
420 newcutfrom = None
421 if vstart > 1:
422 if vend > 1:
423 break
424 # 1 = vstart + (vend - vstart) * cutfrom
425 try:
426 newcutfrom = (1 - vstart)/(vend - vstart)
427 except ArithmeticError:
428 break
429 if vstart < 0:
430 if vend < 0:
431 break
432 # 0 = vstart + (vend - vstart) * cutfrom
433 try:
434 newcutfrom = - vstart/(vend - vstart)
435 except ArithmeticError:
436 break
437 if newcutfrom is not None and newcutfrom > cutfrom:
438 cutfrom = newcutfrom
439 newcutto = None
440 if vend > 1:
441 # 1 = vstart + (vend - vstart) * cutto
442 try:
443 newcutto = (1 - vstart)/(vend - vstart)
444 except ArithmeticError:
445 break
446 if vend < 0:
447 # 0 = vstart + (vend - vstart) * cutto
448 try:
449 newcutto = - vstart/(vend - vstart)
450 except ArithmeticError:
451 break
452 if newcutto is not None and newcutto < cutto:
453 cutto = newcutto
454 else:
455 if cutfrom < cutto:
456 cutfromvpos = []
457 cuttovpos = []
458 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
459 cutfromvpos.append(vstart + (vend - vstart) * cutfrom)
460 cuttovpos.append(vstart + (vend - vstart) * cutto)
461 styledata.linebasepoints.append(styledata.graph.vpos_pt(*cutfromvpos))
462 styledata.linebasepoints.append(styledata.graph.vpos_pt(*cuttovpos))
463 styledata.validvpos = 0 # clear linebasepoints below
464 styledata.lastvpos = styledata.vpos
465 else:
466 styledata.lastvpos = None
468 def addpointstopath(self, styledata):
469 # add baselinepoints to styledata.path
470 if len(styledata.linebasepoints) > 1:
471 styledata.path.append(path.moveto_pt(*styledata.linebasepoints[0]))
472 if len(styledata.linebasepoints) > 2:
473 styledata.path.append(path.multilineto_pt(styledata.linebasepoints[1:]))
474 else:
475 styledata.path.append(path.lineto_pt(*styledata.linebasepoints[1]))
476 styledata.linebasepoints = []
478 def donedrawpoints(self, graph, styledata):
479 self.addpointstopath(styledata)
480 if styledata.lineattrs is not None and len(styledata.path.path):
481 styledata.linecanvas.stroke(styledata.path, styledata.lineattrs)
483 def drawpoint(self, graph, styledata):
484 self.appendlinebasepoints(graph, styledata)
485 if not styledata.validvpos:
486 self.addpointstopath(styledata)
489 class errorbars(_style):
491 defaulterrorbarattrs = []
493 def __init__(self, size=0.1*unit.v_cm,
494 errorbarattrs=[],
495 epsilon=1e-10):
496 self.size = size
497 self.errorbarattrs = errorbarattrs
498 self.epsilon = epsilon
500 def selectstyle(self, selectindex, selecttotal, styledata):
501 styledata.errorsize_pt = unit.topt(attr.selectattr(self.size, selectindex, selecttotal))
502 styledata.errorbarattrs = attr.selectattrs(self.defaulterrorbarattrs + self.errorbarattrs, selectindex, selecttotal)
504 def initdrawpoints(self, graph, styledata):
505 styledata.errorbarcanvas = graph.insert(canvas.canvas())
506 styledata.errorlist = []
507 if styledata.errorbarattrs is not None:
508 axisindex = 0
509 for axisname in graph.axisnames:
510 if styledata.index[axisname].keys() != ["x"]:
511 styledata.errorlist.append((axisname, axisindex))
512 axisindex += 1
514 def doerrorbars(self, styledata):
515 # errorbar loop over the different direction having errorbars
516 for erroraxisname, erroraxisindex in styledata.errorlist:
517 # create path for errorbars
518 errorpath = path.path()
519 errorpath += styledata.graph.vgeodesic(*(vminpos + vmaxpos))
520 for vcap in vcaps:
521 for axisname in styledata.graph.axisnames:
522 if axisname != erroraxisname:
523 errorpath += styledata.graph.vcap_pt(axisname, styledata.errorsize_pt, *vcap)
525 # stroke errorpath
526 if len(errorpath.path):
527 styledata.errorbarcanvas.stroke(errorpath, styledata.errorbarattrs)
529 def drawpoint(self, graph, styledata):
530 self.doerrorbars(styledata)
533 class text(symbol):
535 defaulttextattrs = [textmodule.halign.center, textmodule.vshift.mathaxis]
537 def __init__(self, textdx=0*unit.v_cm, textdy=0.3*unit.v_cm, textattrs=[], **kwargs):
538 self.textdx = textdx
539 self.textdy = textdy
540 self.textattrs = textattrs
541 symbol.__init__(self, **kwargs)
543 def setdata(self, graph, columns, styledata):
544 columns = columns.copy()
545 styledata.textindex = columns["text"]
546 del columns["text"]
547 return symbol.setdata(self, graph, columns, styledata)
549 def selectstyle(self, selectindex, selecttotal, styledata):
550 if self.textattrs is not None:
551 styledata.textattrs = attr.selectattrs(self.defaulttextattrs + self.textattrs, selectindex, selecttotal)
552 else:
553 styledata.textattrs = None
554 symbol.selectstyle(self, selectindex, selecttotal, styledata)
556 def drawsymbol_pt(self, c, x, y, styledata, point=None):
557 symbol.drawsymbol_pt(self, c, x, y, styledata, point)
558 if None not in (x, y, point[styledata.textindex]) and styledata.textattrs is not None:
559 c.text_pt(x + styledata.textdx_pt, y + styledata.textdy_pt, str(point[styledata.textindex]), styledata.textattrs)
561 def drawpoints(self, points, graph, styledata):
562 styledata.textdx_pt = unit.topt(self.textdx)
563 styledata.textdy_pt = unit.topt(self.textdy)
564 symbol.drawpoints(self, points, graph, styledata)
567 class arrow(_style):
569 defaultlineattrs = []
570 defaultarrowattrs = []
572 def __init__(self, linelength=0.25*unit.v_cm, arrowsize=0.15*unit.v_cm, lineattrs=[], arrowattrs=[], epsilon=1e-10):
573 self.linelength = linelength
574 self.arrowsize = arrowsize
575 self.lineattrs = lineattrs
576 self.arrowattrs = arrowattrs
577 self.epsilon = epsilon
579 def setdata(self, graph, columns, styledata):
580 if len(graph.axisnames) != 2:
581 raise TypeError("arrow style restricted on two-dimensional graphs")
582 columns = columns.copy()
583 styledata.xaxis, styledata.xindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[0]))
584 styledata.yaxis, styledata.yindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[1]))
585 styledata.sizeindex = columns["size"]
586 del columns["size"]
587 styledata.angleindex = columns["angle"]
588 del columns["angle"]
589 return columns
591 def adjustaxes(self, points, columns, styledata):
592 if styledata.xindex in columns:
593 styledata.xaxis.adjustrange(points, styledata.xindex)
594 if styledata.yindex in columns:
595 styledata.yaxis.adjustrange(points, styledata.yindex)
597 def selectstyle(self, selectindex, selecttotal, styledata):
598 if self.lineattrs is not None:
599 styledata.lineattrs = attr.selectattrs(self.defaultlineattrs + self.lineattrs, selectindex, selecttotal)
600 else:
601 styledata.lineattrs = None
602 if self.arrowattrs is not None:
603 styledata.arrowattrs = attr.selectattrs(self.defaultarrowattrs + self.arrowattrs, selectindex, selecttotal)
604 else:
605 styledata.arrowattrs = None
607 def drawpoints(self, points, graph, styledata):
608 if styledata.lineattrs is not None and styledata.arrowattrs is not None:
609 linelength_pt = unit.topt(self.linelength)
610 for point in points:
611 xpos, ypos = graph.pos_pt(point[styledata.xindex], point[styledata.yindex], xaxis=styledata.xaxis, yaxis=styledata.yaxis)
612 if point[styledata.sizeindex] > self.epsilon:
613 dx = math.cos(point[styledata.angleindex]*math.pi/180)
614 dy = math.sin(point[styledata.angleindex]*math.pi/180)
615 x1 = xpos-0.5*dx*linelength_pt*point[styledata.sizeindex]
616 y1 = ypos-0.5*dy*linelength_pt*point[styledata.sizeindex]
617 x2 = xpos+0.5*dx*linelength_pt*point[styledata.sizeindex]
618 y2 = ypos+0.5*dy*linelength_pt*point[styledata.sizeindex]
619 graph.stroke(path.line_pt(x1, y1, x2, y2), styledata.lineattrs +
620 [deco.earrow(styledata.arrowattrs, size=self.arrowsize*point[styledata.sizeindex])])
623 class rect(_style):
625 def __init__(self, palette=color.palette.Gray):
626 self.palette = palette
628 def setdata(self, graph, columns, styledata):
629 if len(graph.axisnames) != 2:
630 raise TypeError("arrow style restricted on two-dimensional graphs")
631 columns = columns.copy()
632 styledata.xaxis, styledata.xminindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.axisnames[0]))
633 styledata.yaxis, styledata.yminindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.axisnames[1]))
634 xaxis, styledata.xmaxindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.axisnames[0]))
635 yaxis, styledata.ymaxindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.axisnames[1]))
636 if xaxis != styledata.xaxis or yaxis != styledata.yaxis:
637 raise ValueError("min/max values should use the same axes")
638 styledata.colorindex = columns["color"]
639 del columns["color"]
640 return columns
642 def selectstyle(self, selectindex, selecttotal, styledata):
643 pass
645 def adjustaxes(self, points, columns, styledata):
646 if styledata.xminindex in columns:
647 styledata.xaxis.adjustrange(points, styledata.xminindex)
648 if styledata.xmaxindex in columns:
649 styledata.xaxis.adjustrange(points, styledata.xmaxindex)
650 if styledata.yminindex in columns:
651 styledata.yaxis.adjustrange(points, styledata.yminindex)
652 if styledata.ymaxindex in columns:
653 styledata.yaxis.adjustrange(points, styledata.ymaxindex)
655 def drawpoints(self, points, graph, styledata):
656 # TODO: bbox shortcut
657 c = graph.insert(canvas.canvas())
658 lastcolorvalue = None
659 for point in points:
660 try:
661 xvmin = styledata.xaxis.convert(point[styledata.xminindex])
662 xvmax = styledata.xaxis.convert(point[styledata.xmaxindex])
663 yvmin = styledata.yaxis.convert(point[styledata.yminindex])
664 yvmax = styledata.yaxis.convert(point[styledata.ymaxindex])
665 colorvalue = point[styledata.colorindex]
666 if colorvalue != lastcolorvalue:
667 color = self.palette.getcolor(point[styledata.colorindex])
668 except:
669 continue
670 if ((xvmin < 0 and xvmax < 0) or (xvmin > 1 and xvmax > 1) or
671 (yvmin < 0 and yvmax < 0) or (yvmin > 1 and yvmax > 1)):
672 continue
673 if xvmin < 0:
674 xvmin = 0
675 elif xvmin > 1:
676 xvmin = 1
677 if xvmax < 0:
678 xvmax = 0
679 elif xvmax > 1:
680 xvmax = 1
681 if yvmin < 0:
682 yvmin = 0
683 elif yvmin > 1:
684 yvmin = 1
685 if yvmax < 0:
686 yvmax = 0
687 elif yvmax > 1:
688 yvmax = 1
689 p = graph.vgeodesic(xvmin, yvmin, xvmax, yvmin)
690 p.append(graph.vgeodesic_el(xvmax, yvmin, xvmax, yvmax))
691 p.append(graph.vgeodesic_el(xvmax, yvmax, xvmin, yvmax))
692 p.append(graph.vgeodesic_el(xvmin, yvmax, xvmin, yvmin))
693 p.append(path.closepath())
694 if colorvalue != lastcolorvalue:
695 c.set([color])
696 c.fill(p)
698 class bar(_style):
700 defaultfrompathattrs = []
701 defaultbarattrs = [color.palette.Rainbow, deco.stroked([color.gray.black])]
703 def __init__(self, fromvalue=None, frompathattrs=[], barattrs=[], subnames=None, epsilon=1e-10):
704 self.fromvalue = fromvalue
705 self.frompathattrs = frompathattrs
706 self.barattrs = barattrs
707 self.subnames = subnames
708 self.epsilon = epsilon
710 def setdata(self, graph, columns, styledata):
711 # TODO: remove limitation to 2d graphs
712 if len(graph.axisnames) != 2:
713 raise TypeError("arrow style currently restricted on two-dimensional graphs")
714 columns = columns.copy()
715 xvalue = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[0]))
716 yvalue = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[1]))
717 if (xvalue is None and yvalue is None) or (xvalue is not None and yvalue is not None):
718 raise TypeError("must specify exactly one value axis")
719 if xvalue is not None:
720 styledata.valuepos = 0
721 styledata.nameaxis, styledata.nameindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)name$" % graph.axisnames[1]))
722 styledata.valueaxis = xvalue[0]
723 styledata.valueindices = [xvalue[1]]
724 else:
725 styledata.valuepos = 1
726 styledata.nameaxis, styledata.nameindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)name$" % graph.axisnames[0]))
727 styledata.valueaxis = yvalue[0]
728 styledata.valueindices = [yvalue[1]]
729 i = 1
730 while 1:
731 try:
732 valueaxis, valueindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)stack%i$" % (graph.axisnames[styledata.valuepos], i)))
733 except:
734 break
735 if styledata.valueaxis != valueaxis:
736 raise ValueError("different value axes for stacked bars")
737 styledata.valueindices.append(valueindex)
738 i += 1
739 return columns
741 def selectstyle(self, selectindex, selecttotal, styledata):
742 if selectindex:
743 styledata.frompathattrs = None
744 else:
745 styledata.frompathattrs = self.defaultfrompathattrs + self.frompathattrs
746 if selecttotal > 1:
747 if self.barattrs is not None:
748 styledata.barattrs = attr.selectattrs(self.defaultbarattrs + self.barattrs, selectindex, selecttotal)
749 else:
750 styledata.barattrs = None
751 else:
752 styledata.barattrs = self.defaultbarattrs + self.barattrs
753 styledata.selectindex = selectindex
754 styledata.selecttotal = selecttotal
755 if styledata.selecttotal != 1 and self.subnames is not None:
756 raise ValueError("subnames not allowed when iterating over bars")
758 def adjustaxes(self, points, columns, styledata):
759 if styledata.nameindex in columns:
760 if styledata.selecttotal == 1:
761 styledata.nameaxis.adjustrange(points, styledata.nameindex, subnames=self.subnames)
762 else:
763 for i in range(styledata.selecttotal):
764 styledata.nameaxis.adjustrange(points, styledata.nameindex, subnames=[i])
765 for valueindex in styledata.valueindices:
766 if valueindex in columns:
767 styledata.valueaxis.adjustrange(points, valueindex)
769 def drawpoints(self, points, graph, styledata):
770 if self.fromvalue is not None:
771 vfromvalue = styledata.valueaxis.convert(self.fromvalue)
772 if vfromvalue < -self.epsilon:
773 vfromvalue = 0
774 if vfromvalue > 1 + self.epsilon:
775 vfromvalue = 1
776 if styledata.frompathattrs is not None and vfromvalue > self.epsilon and vfromvalue < 1 - self.epsilon:
777 if styledata.valuepos:
778 p = graph.vgeodesic(0, vfromvalue, 1, vfromvalue)
779 else:
780 p = graph.vgeodesic(vfromvalue, 0, vfromvalue, 1)
781 graph.stroke(p, styledata.frompathattrs)
782 else:
783 vfromvalue = 0
784 l = len(styledata.valueindices)
785 if l > 1:
786 barattrslist = []
787 for i in range(l):
788 barattrslist.append(attr.selectattrs(styledata.barattrs, i, l))
789 else:
790 barattrslist = [styledata.barattrs]
791 for point in points:
792 vvaluemax = vfromvalue
793 for valueindex, barattrs in zip(styledata.valueindices, barattrslist):
794 vvaluemin = vvaluemax
795 try:
796 vvaluemax = styledata.valueaxis.convert(point[valueindex])
797 except:
798 continue
800 if styledata.selecttotal == 1:
801 try:
802 vnamemin = styledata.nameaxis.convert((point[styledata.nameindex], 0))
803 except:
804 continue
805 try:
806 vnamemax = styledata.nameaxis.convert((point[styledata.nameindex], 1))
807 except:
808 continue
809 else:
810 try:
811 vnamemin = styledata.nameaxis.convert((point[styledata.nameindex], styledata.selectindex, 0))
812 except:
813 continue
814 try:
815 vnamemax = styledata.nameaxis.convert((point[styledata.nameindex], styledata.selectindex, 1))
816 except:
817 continue
819 if styledata.valuepos:
820 p = graph.vgeodesic(vnamemin, vvaluemin, vnamemin, vvaluemax)
821 p.append(graph.vgeodesic_el(vnamemin, vvaluemax, vnamemax, vvaluemax))
822 p.append(graph.vgeodesic_el(vnamemax, vvaluemax, vnamemax, vvaluemin))
823 p.append(graph.vgeodesic_el(vnamemax, vvaluemin, vnamemin, vvaluemin))
824 p.append(path.closepath())
825 else:
826 p = graph.vgeodesic(vvaluemin, vnamemin, vvaluemin, vnamemax)
827 p.append(graph.vgeodesic_el(vvaluemin, vnamemax, vvaluemax, vnamemax))
828 p.append(graph.vgeodesic_el(vvaluemax, vnamemax, vvaluemax, vnamemin))
829 p.append(graph.vgeodesic_el(vvaluemax, vnamemin, vvaluemin, vnamemin))
830 p.append(path.closepath())
831 if barattrs is not None:
832 graph.fill(p, barattrs)
834 def key_pt(self, c, x_pt, y_pt, width_pt, height_pt, styledata):
835 l = len(styledata.valueindices)
836 if l > 1:
837 for i in range(l):
838 c.fill(path.rect_pt(x_pt+i*width_pt/l, y_pt, width_pt/l, height_pt), attr.selectattrs(styledata.barattrs, i, l))
839 else:
840 c.fill(path.rect_pt(x_pt, y_pt, width_pt, height_pt), styledata.barattrs)