graph style/data rework -- basic concept seems to work well now, various parts need...
[PyX/mjg.git] / pyx / graph / style.py
blob926a6c5c60ba6b68dc8199e6921eb88e66c1fce0
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 like staticmethods
370 cross = _crosssymbol
371 plus = _plussymbol
372 square = _squaresymbol
373 triangle = _trianglesymbol
374 circle = _circlesymbol
375 diamond = _diamondsymbol
377 changecross = attr.changelist([cross, plus, square, triangle, circle, diamond])
378 changeplus = attr.changelist([plus, square, triangle, circle, diamond, cross])
379 changesquare = attr.changelist([square, triangle, circle, diamond, cross, plus])
380 changetriangle = attr.changelist([triangle, circle, diamond, cross, plus, square])
381 changecircle = attr.changelist([circle, diamond, cross, plus, square, triangle])
382 changediamond = attr.changelist([diamond, cross, plus, square, triangle, circle])
383 changesquaretwice = attr.changelist([square, square, triangle, triangle, circle, circle, diamond, diamond])
384 changetriangletwice = attr.changelist([triangle, triangle, circle, circle, diamond, diamond, square, square])
385 changecircletwice = attr.changelist([circle, circle, diamond, diamond, square, square, triangle, triangle])
386 changediamondtwice = attr.changelist([diamond, diamond, square, square, triangle, triangle, circle, circle])
388 changestrokedfilled = attr.changelist([deco.stroked, deco.filled])
389 changefilledstroked = attr.changelist([deco.filled, deco.stroked])
391 defaultsymbolattrs = [deco.stroked]
393 def __init__(self, symbol=changecross, size=0.2*unit.v_cm, symbolattrs=[]):
394 self.symbol = symbol
395 self.size = size
396 self.symbolattrs = symbolattrs
398 def selectstyle(self, styledata, graph, selectindex, selecttotal):
399 styledata.symbol = attr.selectattr(self.symbol, selectindex, selecttotal)
400 styledata.size_pt = unit.topt(attr.selectattr(self.size, selectindex, selecttotal))
401 if self.symbolattrs is not None:
402 styledata.symbolattrs = attr.selectattrs(self.defaultsymbolattrs + self.symbolattrs, selectindex, selecttotal)
403 else:
404 styledata.symbolattrs = None
406 def initdrawpoints(self, styledata, graph):
407 styledata.symbolcanvas = graph.insert(canvas.canvas())
409 def drawpoint(self, styledata, graph):
410 if styledata.vposvalid and styledata.symbolattrs is not None:
411 xpos, ypos = graph.vpos_pt(*styledata.vpos)
412 styledata.symbol(styledata.symbolcanvas, xpos, ypos, styledata.size_pt, styledata.symbolattrs)
414 def key_pt(self, styledata, graph, x_pt, y_pt, width_pt, height_pt):
415 if styledata.symbolattrs is not None:
416 styledata.symbol(graph, x_pt+0.5*width_pt, y_pt+0.5*height_pt, styledata.size_pt, styledata.symbolattrs)
419 class line(_styleneedingpointpos):
421 need = ["vpos", "vposmissing", "vposavailable", "vposvalid"]
423 changelinestyle = attr.changelist([style.linestyle.solid,
424 style.linestyle.dashed,
425 style.linestyle.dotted,
426 style.linestyle.dashdotted])
428 defaultlineattrs = [changelinestyle]
430 def __init__(self, lineattrs=[]):
431 self.lineattrs = lineattrs
433 def selectstyle(self, styledata, graph, selectindex, selecttotal):
434 styledata.lineattrs = attr.selectattrs(self.defaultlineattrs + self.lineattrs, selectindex, selecttotal)
436 def initdrawpoints(self, styledata, graph):
437 styledata.linecanvas = graph.insert(canvas.canvas())
438 if styledata.lineattrs is not None:
439 styledata.linecanvas.set(styledata.lineattrs)
440 styledata.path = path.path()
441 styledata.linebasepoints = []
442 styledata.lastvpos = None
444 def addpointstopath(self, styledata):
445 # add baselinepoints to styledata.path
446 if len(styledata.linebasepoints) > 1:
447 styledata.path.append(path.moveto_pt(*styledata.linebasepoints[0]))
448 if len(styledata.linebasepoints) > 2:
449 styledata.path.append(path.multilineto_pt(styledata.linebasepoints[1:]))
450 else:
451 styledata.path.append(path.lineto_pt(*styledata.linebasepoints[1]))
452 styledata.linebasepoints = []
454 def drawpoint(self, styledata, graph):
455 # append linebasepoints
456 if styledata.vposavailable:
457 if len(styledata.linebasepoints):
458 # the last point was inside the graph
459 if styledata.vposvalid: # shortcut for the common case
460 styledata.linebasepoints.append(graph.vpos_pt(*styledata.vpos))
461 else:
462 # cut end
463 cut = 1
464 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
465 newcut = None
466 if vend > 1:
467 # 1 = vstart + (vend - vstart) * cut
468 try:
469 newcut = (1 - vstart)/(vend - vstart)
470 except ArithmeticError:
471 break
472 if vend < 0:
473 # 0 = vstart + (vend - vstart) * cut
474 try:
475 newcut = - vstart/(vend - vstart)
476 except ArithmeticError:
477 break
478 if newcut is not None and newcut < cut:
479 cut = newcut
480 else:
481 cutvpos = []
482 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
483 cutvpos.append(vstart + (vend - vstart) * cut)
484 styledata.linebasepoints.append(styledata.graph.vpos_pt(*cutvpos))
485 self.addpointstopath(styledata)
486 else:
487 # the last point was outside the graph
488 if styledata.lastvpos is not None:
489 if styledata.vposvalid:
490 # cut beginning
491 cut = 0
492 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
493 newcut = None
494 if vstart > 1:
495 # 1 = vstart + (vend - vstart) * cut
496 try:
497 newcut = (1 - vstart)/(vend - vstart)
498 except ArithmeticError:
499 break
500 if vstart < 0:
501 # 0 = vstart + (vend - vstart) * cut
502 try:
503 newcut = - vstart/(vend - vstart)
504 except ArithmeticError:
505 break
506 if newcut is not None and newcut > cut:
507 cut = newcut
508 else:
509 cutvpos = []
510 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
511 cutvpos.append(vstart + (vend - vstart) * cut)
512 styledata.linebasepoints.append(graph.vpos_pt(*cutvpos))
513 styledata.linebasepoints.append(graph.vpos_pt(*styledata.vpos))
514 else:
515 # sometimes cut beginning and end
516 cutfrom = 0
517 cutto = 1
518 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
519 newcutfrom = None
520 if vstart > 1:
521 if vend > 1:
522 break
523 # 1 = vstart + (vend - vstart) * cutfrom
524 try:
525 newcutfrom = (1 - vstart)/(vend - vstart)
526 except ArithmeticError:
527 break
528 if vstart < 0:
529 if vend < 0:
530 break
531 # 0 = vstart + (vend - vstart) * cutfrom
532 try:
533 newcutfrom = - vstart/(vend - vstart)
534 except ArithmeticError:
535 break
536 if newcutfrom is not None and newcutfrom > cutfrom:
537 cutfrom = newcutfrom
538 newcutto = None
539 if vend > 1:
540 # 1 = vstart + (vend - vstart) * cutto
541 try:
542 newcutto = (1 - vstart)/(vend - vstart)
543 except ArithmeticError:
544 break
545 if vend < 0:
546 # 0 = vstart + (vend - vstart) * cutto
547 try:
548 newcutto = - vstart/(vend - vstart)
549 except ArithmeticError:
550 break
551 if newcutto is not None and newcutto < cutto:
552 cutto = newcutto
553 else:
554 if cutfrom < cutto:
555 cutfromvpos = []
556 cuttovpos = []
557 for vstart, vend in zip(styledata.lastvpos, styledata.vpos):
558 cutfromvpos.append(vstart + (vend - vstart) * cutfrom)
559 cuttovpos.append(vstart + (vend - vstart) * cutto)
560 styledata.linebasepoints.append(styledata.graph.vpos_pt(*cutfromvpos))
561 styledata.linebasepoints.append(styledata.graph.vpos_pt(*cuttovpos))
562 self.addpointstopath(styledata)
563 styledata.lastvpos = styledata.vpos[:]
564 else:
565 if len(styledata.linebasepoints) > 1:
566 self.addpointstopath(styledata)
567 styledata.lastvpos = None
569 def donedrawpoints(self, styledata, graph):
570 if len(styledata.linebasepoints) > 1:
571 self.addpointstopath(styledata)
572 if styledata.lineattrs is not None and len(styledata.path.path):
573 styledata.linecanvas.stroke(styledata.path)
575 def key_pt(self, styledata, graph, x_pt, y_pt, width_pt, height_pt):
576 if styledata.lineattrs is not None:
577 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)
580 class errorbar(_style):
582 need = ["vpos", "vposmissing", "vposavailable", "vposvalid", "vrange", "vrangemissing"]
584 defaulterrorbarattrs = []
586 def __init__(self, size=0.1*unit.v_cm,
587 errorbarattrs=[],
588 epsilon=1e-10):
589 self.size = size
590 self.errorbarattrs = errorbarattrs
591 self.epsilon = epsilon
593 def columns(self, styledata, graph, columns):
594 for i in styledata.vposmissing:
595 if i in styledata.vrangemissing:
596 raise ValueError("position and range for a graph dimension missing")
597 return []
599 def selectstyle(self, styledata, graph, selectindex, selecttotal):
600 styledata.errorsize_pt = unit.topt(attr.selectattr(self.size, selectindex, selecttotal))
601 styledata.errorbarattrs = attr.selectattrs(self.defaulterrorbarattrs + self.errorbarattrs, selectindex, selecttotal)
603 def initdrawpoints(self, styledata, graph):
604 styledata.errorbarcanvas = graph.insert(canvas.canvas())
605 if styledata.errorbarattrs is not None:
606 styledata.errorbarcanvas.set(styledata.errorbarattrs)
607 styledata.dimensionlist = range(len(styledata.vpos))
609 def drawpoint(self, styledata, graph):
610 if styledata.errorbarattrs is None:
611 return
612 for i in styledata.dimensionlist:
613 for j in styledata.dimensionlist:
614 if (i != j and
615 (styledata.vpos[j] is None or
616 styledata.vpos[j] < -self.epsilon or
617 styledata.vpos[j] > 1+self.epsilon)):
618 break
619 else:
620 if ((styledata.vrange[i][0] is None and styledata.vpos[i] is None) or
621 (styledata.vrange[i][1] is None and styledata.vpos[i] is None) or
622 (styledata.vrange[i][0] is None and styledata.vrange[i][1] is None)):
623 continue
624 vminpos = styledata.vpos[:]
625 if styledata.vrange[i][0] is not None:
626 vminpos[i] = styledata.vrange[i][0]
627 mincap = 1
628 else:
629 mincap = 0
630 if vminpos[i] > 1+self.epsilon:
631 continue
632 if vminpos[i] < -self.epsilon:
633 vminpos[i] = 0
634 mincap = 0
635 vmaxpos = styledata.vpos[:]
636 if styledata.vrange[i][1] is not None:
637 vmaxpos[i] = styledata.vrange[i][1]
638 maxcap = 1
639 else:
640 maxcap = 0
641 if vmaxpos[i] < -self.epsilon:
642 continue
643 if vmaxpos[i] > 1+self.epsilon:
644 vmaxpos[i] = 1
645 maxcap = 0
646 styledata.errorbarcanvas.stroke(graph.vgeodesic(*(vminpos + vmaxpos)))
647 for j in styledata.dimensionlist:
648 if i != j:
649 if mincap:
650 styledata.errorbarcanvas.stroke(graph.vcap_pt(j, styledata.errorsize_pt, *vminpos))
651 if maxcap:
652 styledata.errorbarcanvas.stroke(graph.vcap_pt(j, styledata.errorsize_pt, *vmaxpos))
655 # not yet ported to the new style scheme
657 # class text(symbol):
659 # defaulttextattrs = [textmodule.halign.center, textmodule.vshift.mathaxis]
661 # def __init__(self, textdx=0*unit.v_cm, textdy=0.3*unit.v_cm, textattrs=[], **kwargs):
662 # self.textdx = textdx
663 # self.textdy = textdy
664 # self.textattrs = textattrs
665 # symbol.__init__(self, **kwargs)
667 # def setdata(self, graph, columns, styledata):
668 # columns = columns.copy()
669 # styledata.textindex = columns["text"]
670 # del columns["text"]
671 # return symbol.setdata(self, graph, columns, styledata)
673 # def selectstyle(self, selectindex, selecttotal, styledata):
674 # if self.textattrs is not None:
675 # styledata.textattrs = attr.selectattrs(self.defaulttextattrs + self.textattrs, selectindex, selecttotal)
676 # else:
677 # styledata.textattrs = None
678 # symbol.selectstyle(self, selectindex, selecttotal, styledata)
680 # def drawsymbol_pt(self, c, x, y, styledata, point=None):
681 # symbol.drawsymbol_pt(self, c, x, y, styledata, point)
682 # if None not in (x, y, point[styledata.textindex]) and styledata.textattrs is not None:
683 # c.text_pt(x + styledata.textdx_pt, y + styledata.textdy_pt, str(point[styledata.textindex]), styledata.textattrs)
685 # def drawpoints(self, points, graph, styledata):
686 # styledata.textdx_pt = unit.topt(self.textdx)
687 # styledata.textdy_pt = unit.topt(self.textdy)
688 # symbol.drawpoints(self, points, graph, styledata)
691 # class arrow(_style):
693 # defaultlineattrs = []
694 # defaultarrowattrs = []
696 # def __init__(self, linelength=0.25*unit.v_cm, arrowsize=0.15*unit.v_cm, lineattrs=[], arrowattrs=[], epsilon=1e-10):
697 # self.linelength = linelength
698 # self.arrowsize = arrowsize
699 # self.lineattrs = lineattrs
700 # self.arrowattrs = arrowattrs
701 # self.epsilon = epsilon
703 # def setdata(self, graph, columns, styledata):
704 # if len(graph.axisnames) != 2:
705 # raise TypeError("arrow style restricted on two-dimensional graphs")
706 # columns = columns.copy()
707 # styledata.xaxis, styledata.xindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[0]))
708 # styledata.yaxis, styledata.yindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[1]))
709 # styledata.sizeindex = columns["size"]
710 # del columns["size"]
711 # styledata.angleindex = columns["angle"]
712 # del columns["angle"]
713 # return columns
715 # def adjustaxes(self, points, columns, styledata):
716 # if styledata.xindex in columns:
717 # styledata.xaxis.adjustrange(points, styledata.xindex)
718 # if styledata.yindex in columns:
719 # styledata.yaxis.adjustrange(points, styledata.yindex)
721 # def selectstyle(self, selectindex, selecttotal, styledata):
722 # if self.lineattrs is not None:
723 # styledata.lineattrs = attr.selectattrs(self.defaultlineattrs + self.lineattrs, selectindex, selecttotal)
724 # else:
725 # styledata.lineattrs = None
726 # if self.arrowattrs is not None:
727 # styledata.arrowattrs = attr.selectattrs(self.defaultarrowattrs + self.arrowattrs, selectindex, selecttotal)
728 # else:
729 # styledata.arrowattrs = None
731 # def drawpoints(self, points, graph, styledata):
732 # if styledata.lineattrs is not None and styledata.arrowattrs is not None:
733 # linelength_pt = unit.topt(self.linelength)
734 # for point in points:
735 # xpos, ypos = graph.pos_pt(point[styledata.xindex], point[styledata.yindex], xaxis=styledata.xaxis, yaxis=styledata.yaxis)
736 # if point[styledata.sizeindex] > self.epsilon:
737 # dx = math.cos(point[styledata.angleindex]*math.pi/180)
738 # dy = math.sin(point[styledata.angleindex]*math.pi/180)
739 # x1 = xpos-0.5*dx*linelength_pt*point[styledata.sizeindex]
740 # y1 = ypos-0.5*dy*linelength_pt*point[styledata.sizeindex]
741 # x2 = xpos+0.5*dx*linelength_pt*point[styledata.sizeindex]
742 # y2 = ypos+0.5*dy*linelength_pt*point[styledata.sizeindex]
743 # graph.stroke(path.line_pt(x1, y1, x2, y2), styledata.lineattrs +
744 # [deco.earrow(styledata.arrowattrs, size=self.arrowsize*point[styledata.sizeindex])])
747 # class rect(_style):
749 # def __init__(self, palette=color.palette.Gray):
750 # self.palette = palette
752 # def setdata(self, graph, columns, styledata):
753 # if len(graph.axisnames) != 2:
754 # raise TypeError("arrow style restricted on two-dimensional graphs")
755 # columns = columns.copy()
756 # styledata.xaxis, styledata.xminindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.axisnames[0]))
757 # styledata.yaxis, styledata.yminindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.axisnames[1]))
758 # xaxis, styledata.xmaxindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.axisnames[0]))
759 # yaxis, styledata.ymaxindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.axisnames[1]))
760 # if xaxis != styledata.xaxis or yaxis != styledata.yaxis:
761 # raise ValueError("min/max values should use the same axes")
762 # styledata.colorindex = columns["color"]
763 # del columns["color"]
764 # return columns
766 # def selectstyle(self, selectindex, selecttotal, styledata):
767 # pass
769 # def adjustaxes(self, points, columns, styledata):
770 # if styledata.xminindex in columns:
771 # styledata.xaxis.adjustrange(points, styledata.xminindex)
772 # if styledata.xmaxindex in columns:
773 # styledata.xaxis.adjustrange(points, styledata.xmaxindex)
774 # if styledata.yminindex in columns:
775 # styledata.yaxis.adjustrange(points, styledata.yminindex)
776 # if styledata.ymaxindex in columns:
777 # styledata.yaxis.adjustrange(points, styledata.ymaxindex)
779 # def drawpoints(self, points, graph, styledata):
780 # # TODO: bbox shortcut
781 # c = graph.insert(canvas.canvas())
782 # lastcolorvalue = None
783 # for point in points:
784 # try:
785 # xvmin = styledata.xaxis.convert(point[styledata.xminindex])
786 # xvmax = styledata.xaxis.convert(point[styledata.xmaxindex])
787 # yvmin = styledata.yaxis.convert(point[styledata.yminindex])
788 # yvmax = styledata.yaxis.convert(point[styledata.ymaxindex])
789 # colorvalue = point[styledata.colorindex]
790 # if colorvalue != lastcolorvalue:
791 # color = self.palette.getcolor(point[styledata.colorindex])
792 # except:
793 # continue
794 # if ((xvmin < 0 and xvmax < 0) or (xvmin > 1 and xvmax > 1) or
795 # (yvmin < 0 and yvmax < 0) or (yvmin > 1 and yvmax > 1)):
796 # continue
797 # if xvmin < 0:
798 # xvmin = 0
799 # elif xvmin > 1:
800 # xvmin = 1
801 # if xvmax < 0:
802 # xvmax = 0
803 # elif xvmax > 1:
804 # xvmax = 1
805 # if yvmin < 0:
806 # yvmin = 0
807 # elif yvmin > 1:
808 # yvmin = 1
809 # if yvmax < 0:
810 # yvmax = 0
811 # elif yvmax > 1:
812 # yvmax = 1
813 # p = graph.vgeodesic(xvmin, yvmin, xvmax, yvmin)
814 # p.append(graph.vgeodesic_el(xvmax, yvmin, xvmax, yvmax))
815 # p.append(graph.vgeodesic_el(xvmax, yvmax, xvmin, yvmax))
816 # p.append(graph.vgeodesic_el(xvmin, yvmax, xvmin, yvmin))
817 # p.append(path.closepath())
818 # if colorvalue != lastcolorvalue:
819 # c.set([color])
820 # c.fill(p)
823 # class bar(_style):
825 # defaultfrompathattrs = []
826 # defaultbarattrs = [color.palette.Rainbow, deco.stroked([color.gray.black])]
828 # def __init__(self, fromvalue=None, frompathattrs=[], barattrs=[], subnames=None, epsilon=1e-10):
829 # self.fromvalue = fromvalue
830 # self.frompathattrs = frompathattrs
831 # self.barattrs = barattrs
832 # self.subnames = subnames
833 # self.epsilon = epsilon
835 # def setdata(self, graph, columns, styledata):
836 # # TODO: remove limitation to 2d graphs
837 # if len(graph.axisnames) != 2:
838 # raise TypeError("arrow style currently restricted on two-dimensional graphs")
839 # columns = columns.copy()
840 # xvalue = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[0]))
841 # yvalue = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[1]))
842 # if (xvalue is None and yvalue is None) or (xvalue is not None and yvalue is not None):
843 # raise TypeError("must specify exactly one value axis")
844 # if xvalue is not None:
845 # styledata.valuepos = 0
846 # styledata.nameaxis, styledata.nameindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)name$" % graph.axisnames[1]))
847 # styledata.valueaxis = xvalue[0]
848 # styledata.valueindices = [xvalue[1]]
849 # else:
850 # styledata.valuepos = 1
851 # styledata.nameaxis, styledata.nameindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)name$" % graph.axisnames[0]))
852 # styledata.valueaxis = yvalue[0]
853 # styledata.valueindices = [yvalue[1]]
854 # i = 1
855 # while 1:
856 # try:
857 # valueaxis, valueindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)stack%i$" % (graph.axisnames[styledata.valuepos], i)))
858 # except:
859 # break
860 # if styledata.valueaxis != valueaxis:
861 # raise ValueError("different value axes for stacked bars")
862 # styledata.valueindices.append(valueindex)
863 # i += 1
864 # return columns
866 # def selectstyle(self, selectindex, selecttotal, styledata):
867 # if selectindex:
868 # styledata.frompathattrs = None
869 # else:
870 # styledata.frompathattrs = self.defaultfrompathattrs + self.frompathattrs
871 # if selecttotal > 1:
872 # if self.barattrs is not None:
873 # styledata.barattrs = attr.selectattrs(self.defaultbarattrs + self.barattrs, selectindex, selecttotal)
874 # else:
875 # styledata.barattrs = None
876 # else:
877 # styledata.barattrs = self.defaultbarattrs + self.barattrs
878 # styledata.selectindex = selectindex
879 # styledata.selecttotal = selecttotal
880 # if styledata.selecttotal != 1 and self.subnames is not None:
881 # raise ValueError("subnames not allowed when iterating over bars")
883 # def adjustaxes(self, points, columns, styledata):
884 # if styledata.nameindex in columns:
885 # if styledata.selecttotal == 1:
886 # styledata.nameaxis.adjustrange(points, styledata.nameindex, subnames=self.subnames)
887 # else:
888 # for i in range(styledata.selecttotal):
889 # styledata.nameaxis.adjustrange(points, styledata.nameindex, subnames=[i])
890 # for valueindex in styledata.valueindices:
891 # if valueindex in columns:
892 # styledata.valueaxis.adjustrange(points, valueindex)
894 # def drawpoints(self, points, graph, styledata):
895 # if self.fromvalue is not None:
896 # vfromvalue = styledata.valueaxis.convert(self.fromvalue)
897 # if vfromvalue < -self.epsilon:
898 # vfromvalue = 0
899 # if vfromvalue > 1 + self.epsilon:
900 # vfromvalue = 1
901 # if styledata.frompathattrs is not None and vfromvalue > self.epsilon and vfromvalue < 1 - self.epsilon:
902 # if styledata.valuepos:
903 # p = graph.vgeodesic(0, vfromvalue, 1, vfromvalue)
904 # else:
905 # p = graph.vgeodesic(vfromvalue, 0, vfromvalue, 1)
906 # graph.stroke(p, styledata.frompathattrs)
907 # else:
908 # vfromvalue = 0
909 # l = len(styledata.valueindices)
910 # if l > 1:
911 # barattrslist = []
912 # for i in range(l):
913 # barattrslist.append(attr.selectattrs(styledata.barattrs, i, l))
914 # else:
915 # barattrslist = [styledata.barattrs]
916 # for point in points:
917 # vvaluemax = vfromvalue
918 # for valueindex, barattrs in zip(styledata.valueindices, barattrslist):
919 # vvaluemin = vvaluemax
920 # try:
921 # vvaluemax = styledata.valueaxis.convert(point[valueindex])
922 # except:
923 # continue
925 # if styledata.selecttotal == 1:
926 # try:
927 # vnamemin = styledata.nameaxis.convert((point[styledata.nameindex], 0))
928 # except:
929 # continue
930 # try:
931 # vnamemax = styledata.nameaxis.convert((point[styledata.nameindex], 1))
932 # except:
933 # continue
934 # else:
935 # try:
936 # vnamemin = styledata.nameaxis.convert((point[styledata.nameindex], styledata.selectindex, 0))
937 # except:
938 # continue
939 # try:
940 # vnamemax = styledata.nameaxis.convert((point[styledata.nameindex], styledata.selectindex, 1))
941 # except:
942 # continue
944 # if styledata.valuepos:
945 # p = graph.vgeodesic(vnamemin, vvaluemin, vnamemin, vvaluemax)
946 # p.append(graph.vgeodesic_el(vnamemin, vvaluemax, vnamemax, vvaluemax))
947 # p.append(graph.vgeodesic_el(vnamemax, vvaluemax, vnamemax, vvaluemin))
948 # p.append(graph.vgeodesic_el(vnamemax, vvaluemin, vnamemin, vvaluemin))
949 # p.append(path.closepath())
950 # else:
951 # p = graph.vgeodesic(vvaluemin, vnamemin, vvaluemin, vnamemax)
952 # p.append(graph.vgeodesic_el(vvaluemin, vnamemax, vvaluemax, vnamemax))
953 # p.append(graph.vgeodesic_el(vvaluemax, vnamemax, vvaluemax, vnamemin))
954 # p.append(graph.vgeodesic_el(vvaluemax, vnamemin, vvaluemin, vnamemin))
955 # p.append(path.closepath())
956 # if barattrs is not None:
957 # graph.fill(p, barattrs)
959 # def key_pt(self, c, x_pt, y_pt, width_pt, height_pt, styledata):
960 # l = len(styledata.valueindices)
961 # if l > 1:
962 # for i in range(l):
963 # c.fill(path.rect_pt(x_pt+i*width_pt/l, y_pt, width_pt/l, height_pt), attr.selectattrs(styledata.barattrs, i, l))
964 # else:
965 # c.fill(path.rect_pt(x_pt, y_pt, width_pt, height_pt), styledata.barattrs)