graph arrow style and various style/data/graph related fixes
[PyX/mjg.git] / pyx / graph / style.py
blob06e34e5713c25d0b8327a0153353c493e10455c3
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 math
27 from pyx import attr, deco, style, color, unit, canvas, path
28 from pyx import text as textmodule
31 class _style:
32 """Interface class for graph styles
34 Each graph style must support the methods described in this
35 class. However, since a graph style might not need to perform
36 actions on all the various events, it does not need to overwrite
37 all methods of this base class (e.g. this class is not an abstract
38 class in any respect).
40 A style should never store private data by instance variables
41 (i.e. accessing self), but it should use the styledata instance
42 instead. A style instance can be used multiple times with different
43 styledata instances at the very same time. The styledata instance
44 acts as a data container and furthermore allows for sharing
45 information across several styles.
47 A style contains two class variables, which are not to be
48 modified. provide is a of variable names a style provides via
49 the styledata instance. This list should be used to find, whether
50 all needs of subsequent styles are fullfilled. Otherwise the
51 provider dictionary described below should list a proper style
52 to be inserted. Contrary, need is a list of variable names the
53 style needs to access in the styledata instance."""
55 provide = [] # by default, we provide nothing
56 need = [] # and do not depend on anything
58 def columns(self, styledata, graph, columns):
59 """Set column information
61 This method is used setup the column information to be
62 accessible to the style later on. The style should analyse
63 the list of strings columns, which contain the column names
64 of the data. The method should return a list of column names
65 which the style will make use of."""
66 return []
68 def selectstyle(self, styledata, graph, selectindex, selecttotal):
69 """Select stroke/fill attributes
71 This method is called to allow for the selection of
72 changable attributes of a style."""
73 pass
75 def adjustaxis(self, styledata, graph, column, data, index):
76 """Adjust axis range
78 This method is called in order to adjust the axis range to
79 the provided data. Column is the name of the column (each
80 style is subsequently called for all column names). If index
81 is not None, data is a list of points and index is the index
82 of the column within a point. Otherwise data is already the
83 axis data. Note, that data might be different for different
84 columns, e.i. data might come from various places and is
85 combined without copying but keeping references."""
86 pass
88 def initdrawpoints(self, styledata, graph):
89 """Initialize drawing of data
91 This method might be used to initialize the drawing of data."""
92 pass
94 def drawpoint(self, styledata, graph):
95 """Draw data
97 This method is called for each data point. The data is
98 available in the dictionary styledata.data. The dictionary
99 keys are the column names."""
100 pass
102 def donedrawpoints(self, styledata, graph):
103 """Finalize drawing of data
105 This method is called after the last data point was
106 drawn using the drawpoint method above."""
107 pass
109 def key_pt(self, styledata, graph, x_pt, y_pt, width_pt, height_pt):
110 """Draw graph key
112 This method draws a key for the style to graph at the given
113 position x_pt, y_pt indicating the lower left corner of the
114 given area width_pt, height_pt."""
115 pass
118 # Provider is a dictionary, which maps styledata variable names
119 # to default styles, which provide a default way to create the
120 # corresponding styledata variable. Entries in the provider
121 # dictionary should not depend on other styles, thus the need
122 # list should be empty.
124 provider = {}
127 class _pos(_style):
129 provide = ["vpos", "vposmissing", "vposavailable", "vposvalid"]
131 def __init__(self, epsilon=1e-10):
132 self.epsilon = epsilon
134 def columns(self, styledata, graph, columns):
135 styledata.pointposcolumns = []
136 styledata.vposmissing = []
137 for count, axisnames in enumerate(graph.axesnames):
138 for axisname in axisnames:
139 for column in columns:
140 if axisname == column:
141 styledata.pointposcolumns.append(column)
142 if len(styledata.pointposcolumns) + len(styledata.vposmissing) > count+1:
143 raise ValueError("multiple axes per graph dimension")
144 elif len(styledata.pointposcolumns) + len(styledata.vposmissing) < count+1:
145 styledata.vposmissing.append(count)
146 return styledata.pointposcolumns
148 def adjustaxis(self, styledata, graph, column, data, index):
149 if column in styledata.pointposcolumns:
150 graph.axes[column].adjustrange(data, index)
152 def initdrawpoints(self, styledata, graph):
153 styledata.vpos = [None]*(len(styledata.pointposcolumns) + len(styledata.vposmissing))
154 styledata.pointpostmplist = [[column, index, graph.axes[column]] # temporarily used by drawpoint only
155 for index, column in enumerate(styledata.pointposcolumns)]
156 for missing in styledata.vposmissing:
157 for pointpostmp in styledata.pointpostmplist:
158 if pointpostmp[1] >= missing:
159 pointpostmp[1] += 1
161 def drawpoint(self, styledata, graph):
162 styledata.vposavailable = 1 # valid position (but might be outside of the graph)
163 styledata.vposvalid = 1 # valid position inside the graph
164 for column, index, axis in styledata.pointpostmplist:
165 try:
166 v = axis.convert(styledata.point[column])
167 except (ArithmeticError, ValueError, TypeError):
168 styledata.vposavailable = styledata.vposvalid = 0
169 styledata.vpos[index] = None
170 else:
171 if v < - self.epsilon or v > 1 + self.epsilon:
172 styledata.vposvalid = 0
173 styledata.vpos[index] = v
176 provider["vpos"] = provider["vposmissing"] = provider["vposavailable"] = provider["vposvalid"] = _pos()
179 class _range(_style):
181 provide = ["vrange", "vrangemissing"]
183 # internal bit masks
184 mask_value = 1
185 mask_min = 2
186 mask_max = 4
187 mask_dmin = 8
188 mask_dmax = 16
189 mask_d = 32
191 def __init__(self, epsilon=1e-10):
192 self.epsilon = epsilon
194 def columns(self, styledata, graph, columns):
195 def numberofbits(mask):
196 if not mask:
197 return 0
198 if mask & 1:
199 return numberofbits(mask >> 1) + 1
200 else:
201 return numberofbits(mask >> 1)
202 usecolumns = []
203 styledata.rangeposcolumns = []
204 styledata.vrangemissing = []
205 styledata.rangeposdeltacolumns = {} # temporarily used by adjustaxis only
206 for count, axisnames in enumerate(graph.axesnames):
207 for axisname in axisnames:
208 mask = 0
209 for column in columns:
210 addusecolumns = 1
211 if axisname == column:
212 mask += self.mask_value
213 elif axisname + "min" == column:
214 mask += self.mask_min
215 elif axisname + "max" == column:
216 mask += self.mask_max
217 elif "d" + axisname + "min" == column:
218 mask += self.mask_dmin
219 elif "d" + axisname + "max" == column:
220 mask += self.mask_dmax
221 elif "d" + axisname == column:
222 mask += self.mask_d
223 else:
224 addusecolumns = 0
225 if addusecolumns:
226 usecolumns.append(column)
227 if mask & (self.mask_min | self.mask_max | self.mask_dmin | self.mask_dmax | self.mask_d):
228 if (numberofbits(mask & (self.mask_min | self.mask_dmin | self.mask_d)) > 1 or
229 numberofbits(mask & (self.mask_max | self.mask_dmax | self.mask_d)) > 1):
230 raise ValueError("multiple errorbar definition")
231 if mask & (self.mask_dmin | self.mask_dmax | self.mask_d):
232 if not (mask & self.mask_value):
233 raise ValueError("missing value for delta")
234 styledata.rangeposdeltacolumns[axisname] = {}
235 styledata.rangeposcolumns.append((axisname, mask))
236 elif mask == self.mask_value:
237 usecolumns = usecolumns[:-1]
238 if len(styledata.rangeposcolumns) + len(styledata.vrangemissing) > count+1:
239 raise ValueError("multiple axes per graph dimension")
240 elif len(styledata.rangeposcolumns) + len(styledata.vrangemissing) < count+1:
241 styledata.vrangemissing.append(count)
242 return usecolumns
244 def adjustaxis(self, styledata, graph, column, data, index):
245 if column in [c + "min" for c, m in styledata.rangeposcolumns if m & self.mask_min]:
246 graph.axes[column[:-3]].adjustrange(data, index)
247 if column in [c + "max" for c, m in styledata.rangeposcolumns if m & self.mask_max]:
248 graph.axes[column[:-3]].adjustrange(data, index)
250 # delta handling: fill rangeposdeltacolumns
251 if column in [c for c, m in styledata.rangeposcolumns if m & (self.mask_dmin | self.mask_dmax | self.mask_d)]:
252 styledata.rangeposdeltacolumns[column][self.mask_value] = data, index
253 if column in ["d" + c + "min" for c, m in styledata.rangeposcolumns if m & self.mask_dmin]:
254 styledata.rangeposdeltacolumns[column[1:-3]][self.mask_dmin] = data, index
255 if column in ["d" + c + "max" for c, m in styledata.rangeposcolumns if m & self.mask_dmax]:
256 styledata.rangeposdeltacolumns[column[1:-3]][self.mask_dmax] = data, index
257 if column in ["d" + c for c, m in styledata.rangeposcolumns if m & self.mask_d]:
258 styledata.rangeposdeltacolumns[column[1:]][self.mask_d] = data, index
260 # delta handling: process rangeposdeltacolumns
261 for c, d in styledata.rangeposdeltacolumns.items():
262 if d.has_key(self.mask_value):
263 for k in d.keys():
264 if k != self.mask_value:
265 if k & (self.mask_dmin | self.mask_d):
266 graph.axes[c].adjustrange(d[self.mask_value][0], d[self.mask_value][1],
267 deltamindata=d[k][0], deltaminindex=d[k][1])
268 if k & (self.mask_dmax | self.mask_d):
269 graph.axes[c].adjustrange(d[self.mask_value][0], d[self.mask_value][1],
270 deltamaxdata=d[k][0], deltamaxindex=d[k][1])
271 del d[k]
273 def initdrawpoints(self, styledata, graph):
274 styledata.vrange = [[None for x in range(2)] for y in styledata.rangeposcolumns + styledata.vrangemissing]
275 styledata.rangepostmplist = [[column, mask, index, graph.axes[column]] # temporarily used by drawpoint only
276 for index, (column, mask) in enumerate(styledata.rangeposcolumns)]
277 for missing in styledata.vrangemissing:
278 for rangepostmp in styledata.rangepostmplist:
279 if rangepostmp[2] >= missing:
280 rangepostmp[2] += 1
282 def drawpoint(self, styledata, graph):
283 for column, mask, index, axis in styledata.rangepostmplist:
284 try:
285 if mask & self.mask_min:
286 styledata.vrange[index][0] = axis.convert(styledata.point[column + "min"])
287 if mask & self.mask_dmin:
288 styledata.vrange[index][0] = axis.convert(styledata.point[column] - styledata.point["d" + column + "min"])
289 if mask & self.mask_d:
290 styledata.vrange[index][0] = axis.convert(styledata.point[column] - styledata.point["d" + column])
291 except (ArithmeticError, ValueError, TypeError):
292 styledata.vrange[index][0] = None
293 try:
294 if mask & self.mask_max:
295 styledata.vrange[index][1] = axis.convert(styledata.point[column + "max"])
296 if mask & self.mask_dmax:
297 styledata.vrange[index][1] = axis.convert(styledata.point[column] + styledata.point["d" + column + "max"])
298 if mask & self.mask_d:
299 styledata.vrange[index][1] = axis.convert(styledata.point[column] + styledata.point["d" + column])
300 except (ArithmeticError, ValueError, TypeError):
301 styledata.vrange[index][1] = None
303 # some range checks for data consistency
304 if (styledata.vrange[index][0] is not None and styledata.vrange[index][1] is not None and
305 styledata.vrange[index][0] > styledata.vrange[index][1] + self.epsilon):
306 raise ValueError("negative errorbar range")
307 if (styledata.vrange[index][0] is not None and styledata.vpos[index] is not None and
308 styledata.vrange[index][0] > styledata.vpos[index] + self.epsilon):
309 raise ValueError("negative minimum errorbar")
310 if (styledata.vrange[index][1] is not None and styledata.vpos[index] is not None and
311 styledata.vrange[index][1] < styledata.vpos[index] - self.epsilon):
312 raise ValueError("negative maximum errorbar")
315 provider["vrange"] = provider["vrangemissing"] = _range()
318 def _crosssymbol(c, x_pt, y_pt, size_pt, attrs):
319 c.draw(path.path(path.moveto_pt(x_pt-0.5*size_pt, y_pt-0.5*size_pt),
320 path.lineto_pt(x_pt+0.5*size_pt, y_pt+0.5*size_pt),
321 path.moveto_pt(x_pt-0.5*size_pt, y_pt+0.5*size_pt),
322 path.lineto_pt(x_pt+0.5*size_pt, y_pt-0.5*size_pt)), attrs)
324 def _plussymbol(c, x_pt, y_pt, size_pt, attrs):
325 c.draw(path.path(path.moveto_pt(x_pt-0.707106781*size_pt, y_pt),
326 path.lineto_pt(x_pt+0.707106781*size_pt, y_pt),
327 path.moveto_pt(x_pt, y_pt-0.707106781*size_pt),
328 path.lineto_pt(x_pt, y_pt+0.707106781*size_pt)), attrs)
330 def _squaresymbol(c, x_pt, y_pt, size_pt, attrs):
331 c.draw(path.path(path.moveto_pt(x_pt-0.5*size_pt, y_pt-0.5*size_pt),
332 path.lineto_pt(x_pt+0.5*size_pt, y_pt-0.5*size_pt),
333 path.lineto_pt(x_pt+0.5*size_pt, y_pt+0.5*size_pt),
334 path.lineto_pt(x_pt-0.5*size_pt, y_pt+0.5*size_pt),
335 path.closepath()), attrs)
337 def _trianglesymbol(c, x_pt, y_pt, size_pt, attrs):
338 c.draw(path.path(path.moveto_pt(x_pt-0.759835685*size_pt, y_pt-0.438691337*size_pt),
339 path.lineto_pt(x_pt+0.759835685*size_pt, y_pt-0.438691337*size_pt),
340 path.lineto_pt(x_pt, y_pt+0.877382675*size_pt),
341 path.closepath()), attrs)
343 def _circlesymbol(c, x_pt, y_pt, size_pt, attrs):
344 c.draw(path.path(path.arc_pt(x_pt, y_pt, 0.564189583*size_pt, 0, 360),
345 path.closepath()), attrs)
347 def _diamondsymbol(c, x_pt, y_pt, size_pt, attrs):
348 c.draw(path.path(path.moveto_pt(x_pt-0.537284965*size_pt, y_pt),
349 path.lineto_pt(x_pt, y_pt-0.930604859*size_pt),
350 path.lineto_pt(x_pt+0.537284965*size_pt, y_pt),
351 path.lineto_pt(x_pt, y_pt+0.930604859*size_pt),
352 path.closepath()), attrs)
355 class _styleneedingpointpos(_style):
357 need = ["vposmissing"]
359 def columns(self, styledata, graph, columns):
360 if len(styledata.vposmissing):
361 raise ValueError("position columns incomplete")
362 return []
365 class symbol(_styleneedingpointpos):
367 need = ["vpos", "vposmissing", "vposvalid"]
369 # insert symbols
370 # note, that statements like cross = _crosssymbol are
371 # invalid, since the would lead to unbound methods, but
372 # a single entry changeable list does the trick
373 cross = attr.changelist([_crosssymbol])
374 plus = attr.changelist([_plussymbol])
375 square = attr.changelist([_squaresymbol])
376 triangle = attr.changelist([_trianglesymbol])
377 circle = attr.changelist([_circlesymbol])
378 diamond = attr.changelist([_diamondsymbol])
380 changecross = attr.changelist([_crosssymbol, _plussymbol, _squaresymbol, _trianglesymbol, _circlesymbol, _diamondsymbol])
381 changeplus = attr.changelist([_plussymbol, _squaresymbol, _trianglesymbol, _circlesymbol, _diamondsymbol, cross])
382 changesquare = attr.changelist([_squaresymbol, _trianglesymbol, _circlesymbol, _diamondsymbol, cross, _plussymbol])
383 changetriangle = attr.changelist([_trianglesymbol, _circlesymbol, _diamondsymbol, cross, _plussymbol, _squaresymbol])
384 changecircle = attr.changelist([_circlesymbol, _diamondsymbol, cross, _plussymbol, _squaresymbol, _trianglesymbol])
385 changediamond = attr.changelist([_diamondsymbol, cross, _plussymbol, _squaresymbol, _trianglesymbol, _circlesymbol])
386 changesquaretwice = attr.changelist([_squaresymbol, _squaresymbol, _trianglesymbol, _trianglesymbol, _circlesymbol, _circlesymbol, _diamondsymbol, _diamondsymbol])
387 changetriangletwice = attr.changelist([_trianglesymbol, _trianglesymbol, _circlesymbol, _circlesymbol, _diamondsymbol, _diamondsymbol, _squaresymbol, _squaresymbol])
388 changecircletwice = attr.changelist([_circlesymbol, _circlesymbol, _diamondsymbol, _diamondsymbol, _squaresymbol, _squaresymbol, _trianglesymbol, _trianglesymbol])
389 changediamondtwice = attr.changelist([_diamondsymbol, _diamondsymbol, _squaresymbol, _squaresymbol, _trianglesymbol, _trianglesymbol, _circlesymbol, _circlesymbol])
391 changestrokedfilled = attr.changelist([deco.stroked, deco.filled])
392 changefilledstroked = attr.changelist([deco.filled, deco.stroked])
394 defaultsymbolattrs = [deco.stroked]
396 def __init__(self, symbol=changecross, size=0.2*unit.v_cm, symbolattrs=[]):
397 self.symbol = symbol
398 self.size = size
399 self.symbolattrs = symbolattrs
401 def selectstyle(self, styledata, graph, selectindex, selecttotal):
402 styledata.symbol = attr.selectattr(self.symbol, selectindex, selecttotal)
403 styledata.size_pt = unit.topt(attr.selectattr(self.size, selectindex, selecttotal))
404 if self.symbolattrs is not None:
405 styledata.symbolattrs = attr.selectattrs(self.defaultsymbolattrs + self.symbolattrs, selectindex, selecttotal)
406 else:
407 styledata.symbolattrs = None
409 def initdrawpoints(self, styledata, graph):
410 styledata.symbolcanvas = graph.insert(canvas.canvas())
412 def drawpoint(self, styledata, graph):
413 if styledata.vposvalid and styledata.symbolattrs is not None:
414 xpos, ypos = graph.vpos_pt(*styledata.vpos)
415 styledata.symbol(styledata.symbolcanvas, xpos, ypos, styledata.size_pt, styledata.symbolattrs)
417 def key_pt(self, styledata, graph, x_pt, y_pt, width_pt, height_pt):
418 if styledata.symbolattrs is not None:
419 styledata.symbol(graph, x_pt+0.5*width_pt, y_pt+0.5*height_pt, styledata.size_pt, styledata.symbolattrs)
422 class line(_styleneedingpointpos):
424 need = ["vpos", "vposmissing", "vposavailable", "vposvalid"]
426 changelinestyle = attr.changelist([style.linestyle.solid,
427 style.linestyle.dashed,
428 style.linestyle.dotted,
429 style.linestyle.dashdotted])
431 defaultlineattrs = [changelinestyle]
433 def __init__(self, lineattrs=[]):
434 self.lineattrs = lineattrs
436 def selectstyle(self, styledata, graph, selectindex, selecttotal):
437 styledata.lineattrs = attr.selectattrs(self.defaultlineattrs + self.lineattrs, selectindex, selecttotal)
439 def initdrawpoints(self, styledata, graph):
440 styledata.linecanvas = graph.insert(canvas.canvas())
441 if styledata.lineattrs is not None:
442 styledata.linecanvas.set(styledata.lineattrs)
443 styledata.path = path.path()
444 styledata.linebasepoints = []
445 styledata.lastvpos = None
447 def addpointstopath(self, styledata):
448 # add baselinepoints to styledata.path
449 if len(styledata.linebasepoints) > 1:
450 styledata.path.append(path.moveto_pt(*styledata.linebasepoints[0]))
451 if len(styledata.linebasepoints) > 2:
452 styledata.path.append(path.multilineto_pt(styledata.linebasepoints[1:]))
453 else:
454 styledata.path.append(path.lineto_pt(*styledata.linebasepoints[1]))
455 styledata.linebasepoints = []
457 def drawpoint(self, styledata, graph):
458 # append linebasepoints
459 if styledata.vposavailable:
460 if len(styledata.linebasepoints):
461 # the last point was inside the graph
462 if styledata.vposvalid: # shortcut for the common case
463 styledata.linebasepoints.append(graph.vpos_pt(*styledata.vpos))
464 else:
465 # cut end
466 cut = 1
467 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
468 newcut = None
469 if vend > 1:
470 # 1 = vstart + (vend - vstart) * cut
471 try:
472 newcut = (1 - vstart)/(vend - vstart)
473 except ArithmeticError:
474 break
475 if vend < 0:
476 # 0 = vstart + (vend - vstart) * cut
477 try:
478 newcut = - vstart/(vend - vstart)
479 except ArithmeticError:
480 break
481 if newcut is not None and newcut < cut:
482 cut = newcut
483 else:
484 cutvpos = []
485 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
486 cutvpos.append(vstart + (vend - vstart) * cut)
487 styledata.linebasepoints.append(graph.vpos_pt(*cutvpos))
488 self.addpointstopath(styledata)
489 else:
490 # the last point was outside the graph
491 if styledata.lastvpos is not None:
492 if styledata.vposvalid:
493 # cut beginning
494 cut = 0
495 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
496 newcut = None
497 if vstart > 1:
498 # 1 = vstart + (vend - vstart) * cut
499 try:
500 newcut = (1 - vstart)/(vend - vstart)
501 except ArithmeticError:
502 break
503 if vstart < 0:
504 # 0 = vstart + (vend - vstart) * cut
505 try:
506 newcut = - vstart/(vend - vstart)
507 except ArithmeticError:
508 break
509 if newcut is not None and newcut > cut:
510 cut = newcut
511 else:
512 cutvpos = []
513 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
514 cutvpos.append(vstart + (vend - vstart) * cut)
515 styledata.linebasepoints.append(graph.vpos_pt(*cutvpos))
516 styledata.linebasepoints.append(graph.vpos_pt(*styledata.vpos))
517 else:
518 # sometimes cut beginning and end
519 cutfrom = 0
520 cutto = 1
521 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
522 newcutfrom = None
523 if vstart > 1:
524 if vend > 1:
525 break
526 # 1 = vstart + (vend - vstart) * cutfrom
527 try:
528 newcutfrom = (1 - vstart)/(vend - vstart)
529 except ArithmeticError:
530 break
531 if vstart < 0:
532 if vend < 0:
533 break
534 # 0 = vstart + (vend - vstart) * cutfrom
535 try:
536 newcutfrom = - vstart/(vend - vstart)
537 except ArithmeticError:
538 break
539 if newcutfrom is not None and newcutfrom > cutfrom:
540 cutfrom = newcutfrom
541 newcutto = None
542 if vend > 1:
543 # 1 = vstart + (vend - vstart) * cutto
544 try:
545 newcutto = (1 - vstart)/(vend - vstart)
546 except ArithmeticError:
547 break
548 if vend < 0:
549 # 0 = vstart + (vend - vstart) * cutto
550 try:
551 newcutto = - vstart/(vend - vstart)
552 except ArithmeticError:
553 break
554 if newcutto is not None and newcutto < cutto:
555 cutto = newcutto
556 else:
557 if cutfrom < cutto:
558 cutfromvpos = []
559 cuttovpos = []
560 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
561 cutfromvpos.append(vstart + (vend - vstart) * cutfrom)
562 cuttovpos.append(vstart + (vend - vstart) * cutto)
563 styledata.linebasepoints.append(graph.vpos_pt(*cutfromvpos))
564 styledata.linebasepoints.append(graph.vpos_pt(*cuttovpos))
565 self.addpointstopath(styledata)
566 styledata.lastvpos = styledata.vpos[:]
567 else:
568 if len(styledata.linebasepoints) > 1:
569 self.addpointstopath(styledata)
570 styledata.lastvpos = None
572 def donedrawpoints(self, styledata, graph):
573 if len(styledata.linebasepoints) > 1:
574 self.addpointstopath(styledata)
575 if styledata.lineattrs is not None and len(styledata.path.path):
576 styledata.linecanvas.stroke(styledata.path)
578 def key_pt(self, styledata, graph, x_pt, y_pt, width_pt, height_pt):
579 if styledata.lineattrs is not None:
580 graph.stroke(path.line_pt(x_pt, y_pt+0.5*height_pt, x_pt+width_pt, y_pt+0.5*height_pt), styledata.lineattrs)
583 class errorbar(_style):
585 need = ["vpos", "vposmissing", "vposavailable", "vposvalid", "vrange", "vrangemissing"]
587 defaulterrorbarattrs = []
589 def __init__(self, size=0.1*unit.v_cm,
590 errorbarattrs=[],
591 epsilon=1e-10):
592 self.size = size
593 self.errorbarattrs = errorbarattrs
594 self.epsilon = epsilon
596 def columns(self, styledata, graph, columns):
597 for i in styledata.vposmissing:
598 if i in styledata.vrangemissing:
599 raise ValueError("position and range for a graph dimension missing")
600 return []
602 def selectstyle(self, styledata, graph, selectindex, selecttotal):
603 styledata.errorsize_pt = unit.topt(attr.selectattr(self.size, selectindex, selecttotal))
604 styledata.errorbarattrs = attr.selectattrs(self.defaulterrorbarattrs + self.errorbarattrs, selectindex, selecttotal)
606 def initdrawpoints(self, styledata, graph):
607 styledata.errorbarcanvas = graph.insert(canvas.canvas())
608 if styledata.errorbarattrs is not None:
609 styledata.errorbarcanvas.set(styledata.errorbarattrs)
610 styledata.dimensionlist = range(len(styledata.vpos))
612 def drawpoint(self, styledata, graph):
613 if styledata.errorbarattrs is None:
614 return
615 for i in styledata.dimensionlist:
616 for j in styledata.dimensionlist:
617 if (i != j and
618 (styledata.vpos[j] is None or
619 styledata.vpos[j] < -self.epsilon or
620 styledata.vpos[j] > 1+self.epsilon)):
621 break
622 else:
623 if ((styledata.vrange[i][0] is None and styledata.vpos[i] is None) or
624 (styledata.vrange[i][1] is None and styledata.vpos[i] is None) or
625 (styledata.vrange[i][0] is None and styledata.vrange[i][1] is None)):
626 continue
627 vminpos = styledata.vpos[:]
628 if styledata.vrange[i][0] is not None:
629 vminpos[i] = styledata.vrange[i][0]
630 mincap = 1
631 else:
632 mincap = 0
633 if vminpos[i] > 1+self.epsilon:
634 continue
635 if vminpos[i] < -self.epsilon:
636 vminpos[i] = 0
637 mincap = 0
638 vmaxpos = styledata.vpos[:]
639 if styledata.vrange[i][1] is not None:
640 vmaxpos[i] = styledata.vrange[i][1]
641 maxcap = 1
642 else:
643 maxcap = 0
644 if vmaxpos[i] < -self.epsilon:
645 continue
646 if vmaxpos[i] > 1+self.epsilon:
647 vmaxpos[i] = 1
648 maxcap = 0
649 styledata.errorbarcanvas.stroke(graph.vgeodesic(*(vminpos + vmaxpos)))
650 for j in styledata.dimensionlist:
651 if i != j:
652 if mincap:
653 styledata.errorbarcanvas.stroke(graph.vcap_pt(j, styledata.errorsize_pt, *vminpos))
654 if maxcap:
655 styledata.errorbarcanvas.stroke(graph.vcap_pt(j, styledata.errorsize_pt, *vmaxpos))
658 # class text(symbol):
660 # defaulttextattrs = [textmodule.halign.center, textmodule.vshift.mathaxis]
662 # def __init__(self, textdx=0*unit.v_cm, textdy=0.3*unit.v_cm, textattrs=[], **kwargs):
663 # self.textdx = textdx
664 # self.textdy = textdy
665 # self.textattrs = textattrs
666 # symbol.__init__(self, **kwargs)
668 # def setdata(self, graph, columns, styledata):
669 # columns = columns.copy()
670 # styledata.textindex = columns["text"]
671 # del columns["text"]
672 # return symbol.setdata(self, graph, columns, styledata)
674 # def selectstyle(self, selectindex, selecttotal, styledata):
675 # if self.textattrs is not None:
676 # styledata.textattrs = attr.selectattrs(self.defaulttextattrs + self.textattrs, selectindex, selecttotal)
677 # else:
678 # styledata.textattrs = None
679 # symbol.selectstyle(self, selectindex, selecttotal, styledata)
681 # def drawsymbol_pt(self, c, x, y, styledata, point=None):
682 # symbol.drawsymbol_pt(self, c, x, y, styledata, point)
683 # if None not in (x, y, point[styledata.textindex]) and styledata.textattrs is not None:
684 # c.text_pt(x + styledata.textdx_pt, y + styledata.textdy_pt, str(point[styledata.textindex]), styledata.textattrs)
686 # def drawpoints(self, points, graph, styledata):
687 # styledata.textdx_pt = unit.topt(self.textdx)
688 # styledata.textdy_pt = unit.topt(self.textdy)
689 # symbol.drawpoints(self, points, graph, styledata)
692 class arrow(_styleneedingpointpos):
694 need = ["vpos", "vposmissing", "vposvalid"]
696 defaultlineattrs = []
697 defaultarrowattrs = []
699 def __init__(self, linelength=0.25*unit.v_cm, arrowsize=0.15*unit.v_cm, lineattrs=[], arrowattrs=[], epsilon=1e-10):
700 self.linelength = linelength
701 self.arrowsize = arrowsize
702 self.lineattrs = lineattrs
703 self.arrowattrs = arrowattrs
704 self.epsilon = epsilon
706 def columns(self, styledata, graph, columns):
707 if len(graph.axesnames) != 2:
708 raise ValueError("arrow style restricted on two-dimensional graphs")
709 if "size" not in columns:
710 raise ValueError("size missing")
711 if "angle" not in columns:
712 raise ValueError("angle missing")
713 return ["size", "angle"] + _styleneedingpointpos.columns(self, styledata, graph, columns)
715 def selectstyle(self, styledata, graph, selectindex, selecttotal):
716 if self.lineattrs is not None:
717 styledata.lineattrs = attr.selectattrs(self.defaultlineattrs + self.lineattrs, selectindex, selecttotal)
718 else:
719 styledata.lineattrs = None
720 if self.arrowattrs is not None:
721 styledata.arrowattrs = attr.selectattrs(self.defaultarrowattrs + self.arrowattrs, selectindex, selecttotal)
722 else:
723 styledata.arrowattrs = None
725 def drawpoint(self, styledata, graph):
726 if styledata.lineattrs is not None and styledata.arrowattrs is not None and styledata.vposvalid:
727 linelength_pt = unit.topt(self.linelength)
728 xpos, ypos = graph.vpos_pt(*styledata.vpos)
729 try:
730 angle = styledata.point["angle"] + 0.0
731 size = styledata.point["size"] + 0.0
732 except:
733 pass
734 if styledata.point["size"] > self.epsilon:
735 dx = math.cos(angle*math.pi/180)
736 dy = math.sin(angle*math.pi/180)
737 x1 = xpos-0.5*dx*linelength_pt*size
738 y1 = ypos-0.5*dy*linelength_pt*size
739 x2 = xpos+0.5*dx*linelength_pt*size
740 y2 = ypos+0.5*dy*linelength_pt*size
741 graph.stroke(path.line_pt(x1, y1, x2, y2), styledata.lineattrs +
742 [deco.earrow(styledata.arrowattrs, size=self.arrowsize*size)])
744 def key_pt(self, styledata, graph, x_pt, y_pt, width_pt, height_pt):
745 raise "TODO"
748 # class rect(_style):
750 # def __init__(self, palette=color.palette.Gray):
751 # self.palette = palette
753 # def setdata(self, graph, columns, styledata):
754 # if len(graph.axisnames) != 2:
755 # raise TypeError("arrow style restricted on two-dimensional graphs")
756 # columns = columns.copy()
757 # styledata.xaxis, styledata.xminindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.axisnames[0]))
758 # styledata.yaxis, styledata.yminindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.axisnames[1]))
759 # xaxis, styledata.xmaxindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.axisnames[0]))
760 # yaxis, styledata.ymaxindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.axisnames[1]))
761 # if xaxis != styledata.xaxis or yaxis != styledata.yaxis:
762 # raise ValueError("min/max values should use the same axes")
763 # styledata.colorindex = columns["color"]
764 # del columns["color"]
765 # return columns
767 # def selectstyle(self, selectindex, selecttotal, styledata):
768 # pass
770 # def adjustaxes(self, points, columns, styledata):
771 # if styledata.xminindex in columns:
772 # styledata.xaxis.adjustrange(points, styledata.xminindex)
773 # if styledata.xmaxindex in columns:
774 # styledata.xaxis.adjustrange(points, styledata.xmaxindex)
775 # if styledata.yminindex in columns:
776 # styledata.yaxis.adjustrange(points, styledata.yminindex)
777 # if styledata.ymaxindex in columns:
778 # styledata.yaxis.adjustrange(points, styledata.ymaxindex)
780 # def drawpoints(self, points, graph, styledata):
781 # # TODO: bbox shortcut
782 # c = graph.insert(canvas.canvas())
783 # lastcolorvalue = None
784 # for point in points:
785 # try:
786 # xvmin = styledata.xaxis.convert(point[styledata.xminindex])
787 # xvmax = styledata.xaxis.convert(point[styledata.xmaxindex])
788 # yvmin = styledata.yaxis.convert(point[styledata.yminindex])
789 # yvmax = styledata.yaxis.convert(point[styledata.ymaxindex])
790 # colorvalue = point[styledata.colorindex]
791 # if colorvalue != lastcolorvalue:
792 # color = self.palette.getcolor(point[styledata.colorindex])
793 # except:
794 # continue
795 # if ((xvmin < 0 and xvmax < 0) or (xvmin > 1 and xvmax > 1) or
796 # (yvmin < 0 and yvmax < 0) or (yvmin > 1 and yvmax > 1)):
797 # continue
798 # if xvmin < 0:
799 # xvmin = 0
800 # elif xvmin > 1:
801 # xvmin = 1
802 # if xvmax < 0:
803 # xvmax = 0
804 # elif xvmax > 1:
805 # xvmax = 1
806 # if yvmin < 0:
807 # yvmin = 0
808 # elif yvmin > 1:
809 # yvmin = 1
810 # if yvmax < 0:
811 # yvmax = 0
812 # elif yvmax > 1:
813 # yvmax = 1
814 # p = graph.vgeodesic(xvmin, yvmin, xvmax, yvmin)
815 # p.append(graph.vgeodesic_el(xvmax, yvmin, xvmax, yvmax))
816 # p.append(graph.vgeodesic_el(xvmax, yvmax, xvmin, yvmax))
817 # p.append(graph.vgeodesic_el(xvmin, yvmax, xvmin, yvmin))
818 # p.append(path.closepath())
819 # if colorvalue != lastcolorvalue:
820 # c.set([color])
821 # c.fill(p)
824 # class bar(_style):
826 # defaultfrompathattrs = []
827 # defaultbarattrs = [color.palette.Rainbow, deco.stroked([color.gray.black])]
829 # def __init__(self, fromvalue=None, frompathattrs=[], barattrs=[], subnames=None, epsilon=1e-10):
830 # self.fromvalue = fromvalue
831 # self.frompathattrs = frompathattrs
832 # self.barattrs = barattrs
833 # self.subnames = subnames
834 # self.epsilon = epsilon
836 # def setdata(self, graph, columns, styledata):
837 # # TODO: remove limitation to 2d graphs
838 # if len(graph.axisnames) != 2:
839 # raise TypeError("arrow style currently restricted on two-dimensional graphs")
840 # columns = columns.copy()
841 # xvalue = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[0]))
842 # yvalue = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[1]))
843 # if (xvalue is None and yvalue is None) or (xvalue is not None and yvalue is not None):
844 # raise TypeError("must specify exactly one value axis")
845 # if xvalue is not None:
846 # styledata.valuepos = 0
847 # styledata.nameaxis, styledata.nameindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)name$" % graph.axisnames[1]))
848 # styledata.valueaxis = xvalue[0]
849 # styledata.valueindices = [xvalue[1]]
850 # else:
851 # styledata.valuepos = 1
852 # styledata.nameaxis, styledata.nameindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)name$" % graph.axisnames[0]))
853 # styledata.valueaxis = yvalue[0]
854 # styledata.valueindices = [yvalue[1]]
855 # i = 1
856 # while 1:
857 # try:
858 # valueaxis, valueindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)stack%i$" % (graph.axisnames[styledata.valuepos], i)))
859 # except:
860 # break
861 # if styledata.valueaxis != valueaxis:
862 # raise ValueError("different value axes for stacked bars")
863 # styledata.valueindices.append(valueindex)
864 # i += 1
865 # return columns
867 # def selectstyle(self, selectindex, selecttotal, styledata):
868 # if selectindex:
869 # styledata.frompathattrs = None
870 # else:
871 # styledata.frompathattrs = self.defaultfrompathattrs + self.frompathattrs
872 # if selecttotal > 1:
873 # if self.barattrs is not None:
874 # styledata.barattrs = attr.selectattrs(self.defaultbarattrs + self.barattrs, selectindex, selecttotal)
875 # else:
876 # styledata.barattrs = None
877 # else:
878 # styledata.barattrs = self.defaultbarattrs + self.barattrs
879 # styledata.selectindex = selectindex
880 # styledata.selecttotal = selecttotal
881 # if styledata.selecttotal != 1 and self.subnames is not None:
882 # raise ValueError("subnames not allowed when iterating over bars")
884 # def adjustaxes(self, points, columns, styledata):
885 # if styledata.nameindex in columns:
886 # if styledata.selecttotal == 1:
887 # styledata.nameaxis.adjustrange(points, styledata.nameindex, subnames=self.subnames)
888 # else:
889 # for i in range(styledata.selecttotal):
890 # styledata.nameaxis.adjustrange(points, styledata.nameindex, subnames=[i])
891 # for valueindex in styledata.valueindices:
892 # if valueindex in columns:
893 # styledata.valueaxis.adjustrange(points, valueindex)
895 # def drawpoints(self, points, graph, styledata):
896 # if self.fromvalue is not None:
897 # vfromvalue = styledata.valueaxis.convert(self.fromvalue)
898 # if vfromvalue < -self.epsilon:
899 # vfromvalue = 0
900 # if vfromvalue > 1 + self.epsilon:
901 # vfromvalue = 1
902 # if styledata.frompathattrs is not None and vfromvalue > self.epsilon and vfromvalue < 1 - self.epsilon:
903 # if styledata.valuepos:
904 # p = graph.vgeodesic(0, vfromvalue, 1, vfromvalue)
905 # else:
906 # p = graph.vgeodesic(vfromvalue, 0, vfromvalue, 1)
907 # graph.stroke(p, styledata.frompathattrs)
908 # else:
909 # vfromvalue = 0
910 # l = len(styledata.valueindices)
911 # if l > 1:
912 # barattrslist = []
913 # for i in range(l):
914 # barattrslist.append(attr.selectattrs(styledata.barattrs, i, l))
915 # else:
916 # barattrslist = [styledata.barattrs]
917 # for point in points:
918 # vvaluemax = vfromvalue
919 # for valueindex, barattrs in zip(styledata.valueindices, barattrslist):
920 # vvaluemin = vvaluemax
921 # try:
922 # vvaluemax = styledata.valueaxis.convert(point[valueindex])
923 # except:
924 # continue
926 # if styledata.selecttotal == 1:
927 # try:
928 # vnamemin = styledata.nameaxis.convert((point[styledata.nameindex], 0))
929 # except:
930 # continue
931 # try:
932 # vnamemax = styledata.nameaxis.convert((point[styledata.nameindex], 1))
933 # except:
934 # continue
935 # else:
936 # try:
937 # vnamemin = styledata.nameaxis.convert((point[styledata.nameindex], styledata.selectindex, 0))
938 # except:
939 # continue
940 # try:
941 # vnamemax = styledata.nameaxis.convert((point[styledata.nameindex], styledata.selectindex, 1))
942 # except:
943 # continue
945 # if styledata.valuepos:
946 # p = graph.vgeodesic(vnamemin, vvaluemin, vnamemin, vvaluemax)
947 # p.append(graph.vgeodesic_el(vnamemin, vvaluemax, vnamemax, vvaluemax))
948 # p.append(graph.vgeodesic_el(vnamemax, vvaluemax, vnamemax, vvaluemin))
949 # p.append(graph.vgeodesic_el(vnamemax, vvaluemin, vnamemin, vvaluemin))
950 # p.append(path.closepath())
951 # else:
952 # p = graph.vgeodesic(vvaluemin, vnamemin, vvaluemin, vnamemax)
953 # p.append(graph.vgeodesic_el(vvaluemin, vnamemax, vvaluemax, vnamemax))
954 # p.append(graph.vgeodesic_el(vvaluemax, vnamemax, vvaluemax, vnamemin))
955 # p.append(graph.vgeodesic_el(vvaluemax, vnamemin, vvaluemin, vnamemin))
956 # p.append(path.closepath())
957 # if barattrs is not None:
958 # graph.fill(p, barattrs)
960 # def key_pt(self, c, x_pt, y_pt, width_pt, height_pt, styledata):
961 # l = len(styledata.valueindices)
962 # if l > 1:
963 # for i in range(l):
964 # c.fill(path.rect_pt(x_pt+i*width_pt/l, y_pt, width_pt/l, height_pt), attr.selectattrs(styledata.barattrs, i, l))
965 # else:
966 # c.fill(path.rect_pt(x_pt, y_pt, width_pt, height_pt), styledata.barattrs)