- adjustments to the new graph data+style handling
[PyX/mjg.git] / pyx / graph / style.py
blob294c651c91e8a05e7955e54e5a97221438fa7026
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
30 try:
31 enumerate([])
32 except NameError:
33 # fallback implementation for Python 2.2. and below
34 def enumerate(list):
35 return zip(xrange(len(list)), list)
37 class _style:
38 """Interface class for graph styles
40 Each graph style must support the methods described in this
41 class. However, since a graph style might not need to perform
42 actions on all the various events, it does not need to overwrite
43 all methods of this base class (e.g. this class is not an abstract
44 class in any respect).
46 A style should never store private data by istance variables
47 (i.e. accessing self), but it should use the sharedata and privatedata
48 instances instead. A style instance can be used multiple times with
49 different sharedata and privatedata instances at the very same time.
50 The sharedata and privatedata instances act as data containers and
51 sharedata allows for sharing information across several styles.
53 Every style contains two class variables, which are not to be
54 modified:
55 - providesdata is a list of variable names a style offers via
56 the sharedata instance. This list is used to determine whether
57 all needs of subsequent styles are fullfilled. Otherwise
58 getdefaultprovider should return a proper style to be used.
59 - needsdata is a list of variable names the style needs to access in the
60 sharedata instance.
61 """
63 providesdata = [] # by default, we provide nothing
64 needsdata = [] # and do not depend on anything
66 def columns(self, privatedata, sharedata, graph, columns):
67 """Set column information
69 This method is used setup the column information to be
70 accessible to the style later on. The style should analyse
71 the list of strings columns, which contain the column names
72 of the data. The method should return a list of column names
73 which the style will make use of."""
74 return []
76 def selectstyle(self, privatedata, sharedata, graph, selectindex, selecttotal):
77 """Select stroke/fill attributes
79 This method is called to allow for the selection of
80 changable attributes of a style."""
81 pass
83 def adjustaxis(self, privatedata, sharedata, graph, column, data, index):
84 """Adjust axis range
86 This method is called in order to adjust the axis range to
87 the provided data. Column is the name of the column (each
88 style is subsequently called for all column names). If index
89 is not None, data is a list of points and index is the index
90 of the column within a point. Otherwise data is already the
91 axis data. Note, that data might be different for different
92 columns, e.i. data might come from various places and is
93 combined without copying but keeping references."""
94 pass
96 def initdrawpoints(self, privatedata, sharedata, graph):
97 """Initialize drawing of data
99 This method might be used to initialize the drawing of data."""
100 pass
102 def drawpoint(self, privatedata, sharedata, graph):
103 """Draw data
105 This method is called for each data point. The data is
106 available in the dictionary sharedata.point. The dictionary
107 keys are the column names."""
108 pass
110 def donedrawpoints(self, privatedata, sharedata, graph):
111 """Finalize drawing of data
113 This method is called after the last data point was
114 drawn using the drawpoint method above."""
115 pass
117 def key_pt(self, privatedata, sharedata, graph, x_pt, y_pt, width_pt, height_pt):
118 """Draw graph key"""
121 # The following two methods are used to register and get a default provider
122 # for keys. A key is a variable name in sharedata. A provider is a style
123 # which creates variables in sharedata.
125 _defaultprovider = {}
127 def registerdefaultprovider(style, keys):
128 """sets a style as a default creator for sharedata variables 'keys'"""
129 assert not len(style.needsdata), "currently we state, that a style should not depend on other sharedata variables"
130 for key in keys:
131 assert key in style.providesdata, "key not provided by style"
132 # we might allow for overwriting the defaults, i.e. the following is not checked:
133 # assert key in _defaultprovider.keys(), "default provider already registered for key"
134 _defaultprovider[key] = style
136 def getdefaultprovider(key):
137 """returns a style, which acts as a default creator for the
138 sharedata variable 'key'"""
139 return _defaultprovider[key]
142 class pos(_style):
144 providesdata = ["vpos", "vposmissing", "vposavailable", "vposvalid"]
146 def __init__(self, epsilon=1e-10):
147 self.epsilon = epsilon
149 def columns(self, privatedata, sharedata, graph, columns):
150 privatedata.pointposcolumns = []
151 sharedata.vposmissing = []
152 for count, axisnames in enumerate(graph.axesnames):
153 for axisname in axisnames:
154 for column in columns:
155 if axisname == column:
156 privatedata.pointposcolumns.append(column)
157 if len(privatedata.pointposcolumns) + len(sharedata.vposmissing) > count+1:
158 raise ValueError("multiple axes per graph dimension")
159 elif len(privatedata.pointposcolumns) + len(sharedata.vposmissing) < count+1:
160 sharedata.vposmissing.append(count)
161 return privatedata.pointposcolumns
163 def adjustaxis(self, privatedata, sharedata, graph, column, data, index):
164 if column in privatedata.pointposcolumns:
165 graph.axes[column].adjustrange(data, index)
167 def initdrawpoints(self, privatedata, sharedata, graph):
168 sharedata.vpos = [None]*(len(privatedata.pointposcolumns) + len(sharedata.vposmissing))
169 privatedata.pointpostmplist = [[column, index, graph.axes[column]] # temporarily used by drawpoint only
170 for index, column in enumerate(privatedata.pointposcolumns)]
171 for missing in sharedata.vposmissing:
172 for pointpostmp in privatedata.pointpostmplist:
173 if pointpostmp[1] >= missing:
174 pointpostmp[1] += 1
176 def drawpoint(self, privatedata, sharedata, graph):
177 sharedata.vposavailable = 1 # valid position (but might be outside of the graph)
178 sharedata.vposvalid = 1 # valid position inside the graph
179 for column, index, axis in privatedata.pointpostmplist:
180 try:
181 v = axis.convert(sharedata.point[column])
182 except (ArithmeticError, ValueError, TypeError):
183 sharedata.vposavailable = sharedata.vposvalid = 0
184 sharedata.vpos[index] = None
185 else:
186 if v < -self.epsilon or v > 1+self.epsilon:
187 sharedata.vposvalid = 0
188 sharedata.vpos[index] = v
191 registerdefaultprovider(pos(), pos.providesdata)
194 class range(_style):
196 providesdata = ["vrange", "vrangemissing", "vrangeminmissing", "vrangemaxmissing"]
198 # internal bit masks
199 mask_value = 1
200 mask_min = 2
201 mask_max = 4
202 mask_dmin = 8
203 mask_dmax = 16
204 mask_d = 32
206 def __init__(self, epsilon=1e-10):
207 self.epsilon = epsilon
209 def columns(self, privatedata, sharedata, graph, columns):
210 def numberofbits(mask):
211 if not mask:
212 return 0
213 if mask & 1:
214 return numberofbits(mask >> 1) + 1
215 else:
216 return numberofbits(mask >> 1)
217 usecolumns = []
218 privatedata.rangeposcolumns = []
219 sharedata.vrangemissing = []
220 sharedata.vrangeminmissing = []
221 sharedata.vrangemaxmissing = []
222 privatedata.rangeposdeltacolumns = {} # temporarily used by adjustaxis only
223 for count, axisnames in enumerate(graph.axesnames):
224 for axisname in axisnames:
225 mask = 0
226 for column in columns:
227 addusecolumns = 1
228 if axisname == column:
229 mask += self.mask_value
230 elif axisname + "min" == column:
231 mask += self.mask_min
232 elif axisname + "max" == column:
233 mask += self.mask_max
234 elif "d" + axisname + "min" == column:
235 mask += self.mask_dmin
236 elif "d" + axisname + "max" == column:
237 mask += self.mask_dmax
238 elif "d" + axisname == column:
239 mask += self.mask_d
240 else:
241 addusecolumns = 0
242 if addusecolumns:
243 usecolumns.append(column)
244 if mask & (self.mask_min | self.mask_max | self.mask_dmin | self.mask_dmax | self.mask_d):
245 if (numberofbits(mask & (self.mask_min | self.mask_dmin | self.mask_d)) > 1 or
246 numberofbits(mask & (self.mask_max | self.mask_dmax | self.mask_d)) > 1):
247 raise ValueError("multiple range definition")
248 if mask & (self.mask_dmin | self.mask_dmax | self.mask_d):
249 if not (mask & self.mask_value):
250 raise ValueError("missing value for delta")
251 privatedata.rangeposdeltacolumns[axisname] = {}
252 privatedata.rangeposcolumns.append((axisname, mask))
253 elif mask == self.mask_value:
254 usecolumns = usecolumns[:-1]
255 if len(privatedata.rangeposcolumns) + len(sharedata.vrangemissing) > count+1:
256 raise ValueError("multiple axes per graph dimension")
257 elif len(privatedata.rangeposcolumns) + len(sharedata.vrangemissing) < count+1:
258 sharedata.vrangemissing.append(count)
259 else:
260 if not (privatedata.rangeposcolumns[-1][1] & (self.mask_min | self.mask_dmin | self.mask_d)):
261 sharedata.vrangeminmissing.append(count)
262 if not (privatedata.rangeposcolumns[-1][1] & (self.mask_max | self.mask_dmax | self.mask_d)):
263 sharedata.vrangemaxmissing.append(count)
264 return usecolumns
266 def adjustaxis(self, privatedata, sharedata, graph, column, data, index):
267 if column in [c + "min" for c, m in privatedata.rangeposcolumns if m & self.mask_min]:
268 graph.axes[column[:-3]].adjustrange(data, index)
269 if column in [c + "max" for c, m in privatedata.rangeposcolumns if m & self.mask_max]:
270 graph.axes[column[:-3]].adjustrange(data, index)
272 # delta handling: fill rangeposdeltacolumns
273 if column in [c for c, m in privatedata.rangeposcolumns if m & (self.mask_dmin | self.mask_dmax | self.mask_d)]:
274 privatedata.rangeposdeltacolumns[column][self.mask_value] = data, index
275 if column in ["d" + c + "min" for c, m in privatedata.rangeposcolumns if m & self.mask_dmin]:
276 privatedata.rangeposdeltacolumns[column[1:-3]][self.mask_dmin] = data, index
277 if column in ["d" + c + "max" for c, m in privatedata.rangeposcolumns if m & self.mask_dmax]:
278 privatedata.rangeposdeltacolumns[column[1:-3]][self.mask_dmax] = data, index
279 if column in ["d" + c for c, m in privatedata.rangeposcolumns if m & self.mask_d]:
280 privatedata.rangeposdeltacolumns[column[1:]][self.mask_d] = data, index
282 # delta handling: process rangeposdeltacolumns
283 for c, d in privatedata.rangeposdeltacolumns.items():
284 if d.has_key(self.mask_value):
285 for k in d.keys():
286 if k != self.mask_value:
287 if k & (self.mask_dmin | self.mask_d):
288 graph.axes[c].adjustrange(d[self.mask_value][0], d[self.mask_value][1],
289 deltamindata=d[k][0], deltaminindex=d[k][1])
290 if k & (self.mask_dmax | self.mask_d):
291 graph.axes[c].adjustrange(d[self.mask_value][0], d[self.mask_value][1],
292 deltamaxdata=d[k][0], deltamaxindex=d[k][1])
293 del d[k]
295 def initdrawpoints(self, privatedata, sharedata, graph):
296 sharedata.vrange = [[None for x in xrange(2)] for y in privatedata.rangeposcolumns + sharedata.vrangemissing]
297 privatedata.rangepostmplist = [[column, mask, index, graph.axes[column]] # temporarily used by drawpoint only
298 for index, (column, mask) in enumerate(privatedata.rangeposcolumns)]
299 for missing in sharedata.vrangemissing:
300 for rangepostmp in privatedata.rangepostmplist:
301 if rangepostmp[2] >= missing:
302 rangepostmp[2] += 1
304 def drawpoint(self, privatedata, sharedata, graph):
305 for column, mask, index, axis in privatedata.rangepostmplist:
306 try:
307 if mask & self.mask_min:
308 sharedata.vrange[index][0] = axis.convert(sharedata.point[column + "min"])
309 if mask & self.mask_dmin:
310 sharedata.vrange[index][0] = axis.convert(sharedata.point[column] - sharedata.point["d" + column + "min"])
311 if mask & self.mask_d:
312 sharedata.vrange[index][0] = axis.convert(sharedata.point[column] - sharedata.point["d" + column])
313 except (ArithmeticError, ValueError, TypeError):
314 sharedata.vrange[index][0] = None
315 try:
316 if mask & self.mask_max:
317 sharedata.vrange[index][1] = axis.convert(sharedata.point[column + "max"])
318 if mask & self.mask_dmax:
319 sharedata.vrange[index][1] = axis.convert(sharedata.point[column] + sharedata.point["d" + column + "max"])
320 if mask & self.mask_d:
321 sharedata.vrange[index][1] = axis.convert(sharedata.point[column] + sharedata.point["d" + column])
322 except (ArithmeticError, ValueError, TypeError):
323 sharedata.vrange[index][1] = None
325 # some range checks for data consistency
326 if (sharedata.vrange[index][0] is not None and sharedata.vrange[index][1] is not None and
327 sharedata.vrange[index][0] > sharedata.vrange[index][1] + self.epsilon):
328 raise ValueError("inverse range")
329 #if (sharedata.vrange[index][0] is not None and sharedata.vpos[index] is not None and
330 # sharedata.vrange[index][0] > sharedata.vpos[index] + self.epsilon):
331 # raise ValueError("negative minimum errorbar")
332 #if (sharedata.vrange[index][1] is not None and sharedata.vpos[index] is not None and
333 # sharedata.vrange[index][1] < sharedata.vpos[index] - self.epsilon):
334 # raise ValueError("negative maximum errorbar")
337 registerdefaultprovider(range(), range.providesdata)
340 def _crosssymbol(c, x_pt, y_pt, size_pt, attrs):
341 c.draw(path.path(path.moveto_pt(x_pt-0.5*size_pt, y_pt-0.5*size_pt),
342 path.lineto_pt(x_pt+0.5*size_pt, y_pt+0.5*size_pt),
343 path.moveto_pt(x_pt-0.5*size_pt, y_pt+0.5*size_pt),
344 path.lineto_pt(x_pt+0.5*size_pt, y_pt-0.5*size_pt)), attrs)
346 def _plussymbol(c, x_pt, y_pt, size_pt, attrs):
347 c.draw(path.path(path.moveto_pt(x_pt-0.707106781*size_pt, y_pt),
348 path.lineto_pt(x_pt+0.707106781*size_pt, y_pt),
349 path.moveto_pt(x_pt, y_pt-0.707106781*size_pt),
350 path.lineto_pt(x_pt, y_pt+0.707106781*size_pt)), attrs)
352 def _squaresymbol(c, x_pt, y_pt, size_pt, attrs):
353 c.draw(path.path(path.moveto_pt(x_pt-0.5*size_pt, y_pt-0.5*size_pt),
354 path.lineto_pt(x_pt+0.5*size_pt, y_pt-0.5*size_pt),
355 path.lineto_pt(x_pt+0.5*size_pt, y_pt+0.5*size_pt),
356 path.lineto_pt(x_pt-0.5*size_pt, y_pt+0.5*size_pt),
357 path.closepath()), attrs)
359 def _trianglesymbol(c, x_pt, y_pt, size_pt, attrs):
360 c.draw(path.path(path.moveto_pt(x_pt-0.759835685*size_pt, y_pt-0.438691337*size_pt),
361 path.lineto_pt(x_pt+0.759835685*size_pt, y_pt-0.438691337*size_pt),
362 path.lineto_pt(x_pt, y_pt+0.877382675*size_pt),
363 path.closepath()), attrs)
365 def _circlesymbol(c, x_pt, y_pt, size_pt, attrs):
366 c.draw(path.path(path.arc_pt(x_pt, y_pt, 0.564189583*size_pt, 0, 360),
367 path.closepath()), attrs)
369 def _diamondsymbol(c, x_pt, y_pt, size_pt, attrs):
370 c.draw(path.path(path.moveto_pt(x_pt-0.537284965*size_pt, y_pt),
371 path.lineto_pt(x_pt, y_pt-0.930604859*size_pt),
372 path.lineto_pt(x_pt+0.537284965*size_pt, y_pt),
373 path.lineto_pt(x_pt, y_pt+0.930604859*size_pt),
374 path.closepath()), attrs)
377 class _styleneedingpointpos(_style):
379 needsdata = ["vposmissing"]
381 def columns(self, privatedata, sharedata, graph, columns):
382 if len(sharedata.vposmissing):
383 raise ValueError("position columns incomplete")
384 return []
387 class symbol(_styleneedingpointpos):
389 needsdata = ["vpos", "vposmissing", "vposvalid"]
391 # insert symbols
392 # note, that statements like cross = _crosssymbol are
393 # invalid, since the would lead to unbound methods, but
394 # a single entry changeable list does the trick
395 cross = attr.changelist([_crosssymbol])
396 plus = attr.changelist([_plussymbol])
397 square = attr.changelist([_squaresymbol])
398 triangle = attr.changelist([_trianglesymbol])
399 circle = attr.changelist([_circlesymbol])
400 diamond = attr.changelist([_diamondsymbol])
402 changecross = attr.changelist([_crosssymbol, _plussymbol, _squaresymbol, _trianglesymbol, _circlesymbol, _diamondsymbol])
403 changeplus = attr.changelist([_plussymbol, _squaresymbol, _trianglesymbol, _circlesymbol, _diamondsymbol, cross])
404 changesquare = attr.changelist([_squaresymbol, _trianglesymbol, _circlesymbol, _diamondsymbol, cross, _plussymbol])
405 changetriangle = attr.changelist([_trianglesymbol, _circlesymbol, _diamondsymbol, cross, _plussymbol, _squaresymbol])
406 changecircle = attr.changelist([_circlesymbol, _diamondsymbol, cross, _plussymbol, _squaresymbol, _trianglesymbol])
407 changediamond = attr.changelist([_diamondsymbol, cross, _plussymbol, _squaresymbol, _trianglesymbol, _circlesymbol])
408 changesquaretwice = attr.changelist([_squaresymbol, _squaresymbol, _trianglesymbol, _trianglesymbol, _circlesymbol, _circlesymbol, _diamondsymbol, _diamondsymbol])
409 changetriangletwice = attr.changelist([_trianglesymbol, _trianglesymbol, _circlesymbol, _circlesymbol, _diamondsymbol, _diamondsymbol, _squaresymbol, _squaresymbol])
410 changecircletwice = attr.changelist([_circlesymbol, _circlesymbol, _diamondsymbol, _diamondsymbol, _squaresymbol, _squaresymbol, _trianglesymbol, _trianglesymbol])
411 changediamondtwice = attr.changelist([_diamondsymbol, _diamondsymbol, _squaresymbol, _squaresymbol, _trianglesymbol, _trianglesymbol, _circlesymbol, _circlesymbol])
413 changestrokedfilled = attr.changelist([deco.stroked, deco.filled])
414 changefilledstroked = attr.changelist([deco.filled, deco.stroked])
416 defaultsymbolattrs = [deco.stroked]
418 def __init__(self, symbol=changecross, size=0.2*unit.v_cm, symbolattrs=[]):
419 self.symbol = symbol
420 self.size = size
421 self.symbolattrs = symbolattrs
423 def selectstyle(self, privatedata, sharedata, graph, selectindex, selecttotal):
424 privatedata.symbol = attr.selectattr(self.symbol, selectindex, selecttotal)
425 privatedata.size_pt = unit.topt(attr.selectattr(self.size, selectindex, selecttotal))
426 if self.symbolattrs is not None:
427 privatedata.symbolattrs = attr.selectattrs(self.defaultsymbolattrs + self.symbolattrs, selectindex, selecttotal)
428 else:
429 privatedata.symbolattrs = None
431 def initdrawpoints(self, privatedata, sharedata, graph):
432 privatedata.symbolcanvas = graph.insert(canvas.canvas())
434 def drawpoint(self, privatedata, sharedata, graph):
435 if sharedata.vposvalid and privatedata.symbolattrs is not None:
436 xpos, ypos = graph.vpos_pt(*sharedata.vpos)
437 privatedata.symbol(privatedata.symbolcanvas, xpos, ypos, privatedata.size_pt, privatedata.symbolattrs)
439 def key_pt(self, privatedata, sharedata, graph, x_pt, y_pt, width_pt, height_pt):
440 if privatedata.symbolattrs is not None:
441 privatedata.symbol(graph, x_pt+0.5*width_pt, y_pt+0.5*height_pt, privatedata.size_pt, privatedata.symbolattrs)
444 class line(_styleneedingpointpos):
446 needsdata = ["vpos", "vposmissing", "vposavailable", "vposvalid"]
448 changelinestyle = attr.changelist([style.linestyle.solid,
449 style.linestyle.dashed,
450 style.linestyle.dotted,
451 style.linestyle.dashdotted])
453 defaultlineattrs = [changelinestyle]
455 def __init__(self, lineattrs=[]):
456 self.lineattrs = lineattrs
458 def selectstyle(self, privatedata, sharedata, graph, selectindex, selecttotal):
459 privatedata.lineattrs = attr.selectattrs(self.defaultlineattrs + self.lineattrs, selectindex, selecttotal)
461 def initdrawpoints(self, privatedata, sharedata, graph):
462 if privatedata.lineattrs is not None:
463 privatedata.linecanvas = graph.insert(canvas.canvas())
464 privatedata.linecanvas.set(privatedata.lineattrs)
465 privatedata.path = path.path()
466 privatedata.linebasepoints = []
467 privatedata.lastvpos = None
469 def addpointstopath(self, privatedata, sharedata):
470 # add baselinepoints to privatedata.path
471 if len(privatedata.linebasepoints) > 1:
472 privatedata.path.append(path.moveto_pt(*privatedata.linebasepoints[0]))
473 if len(privatedata.linebasepoints) > 2:
474 privatedata.path.append(path.multilineto_pt(privatedata.linebasepoints[1:]))
475 else:
476 privatedata.path.append(path.lineto_pt(*privatedata.linebasepoints[1]))
477 privatedata.linebasepoints = []
479 def drawpoint(self, privatedata, sharedata, graph):
480 # append linebasepoints
481 if sharedata.vposavailable:
482 if len(privatedata.linebasepoints):
483 # the last point was inside the graph
484 if sharedata.vposvalid: # shortcut for the common case
485 privatedata.linebasepoints.append(graph.vpos_pt(*sharedata.vpos))
486 else:
487 # cut end
488 cut = 1
489 for vstart, vend in zip(privatedata.lastvpos, sharedata.vpos):
490 newcut = None
491 if vend > 1:
492 # 1 = vstart + (vend - vstart) * cut
493 try:
494 newcut = (1 - vstart)/(vend - vstart)
495 except ArithmeticError:
496 break
497 if vend < 0:
498 # 0 = vstart + (vend - vstart) * cut
499 try:
500 newcut = - vstart/(vend - vstart)
501 except ArithmeticError:
502 break
503 if newcut is not None and newcut < cut:
504 cut = newcut
505 else:
506 cutvpos = []
507 for vstart, vend in zip(privatedata.lastvpos, sharedata.vpos):
508 cutvpos.append(vstart + (vend - vstart) * cut)
509 privatedata.linebasepoints.append(graph.vpos_pt(*cutvpos))
510 self.addpointstopath(privatedata, sharedata)
511 else:
512 # the last point was outside the graph
513 if privatedata.lastvpos is not None:
514 if sharedata.vposvalid:
515 # cut beginning
516 cut = 0
517 for vstart, vend in zip(privatedata.lastvpos, sharedata.vpos):
518 newcut = None
519 if vstart > 1:
520 # 1 = vstart + (vend - vstart) * cut
521 try:
522 newcut = (1 - vstart)/(vend - vstart)
523 except ArithmeticError:
524 break
525 if vstart < 0:
526 # 0 = vstart + (vend - vstart) * cut
527 try:
528 newcut = - vstart/(vend - vstart)
529 except ArithmeticError:
530 break
531 if newcut is not None and newcut > cut:
532 cut = newcut
533 else:
534 cutvpos = []
535 for vstart, vend in zip(privatedata.lastvpos, sharedata.vpos):
536 cutvpos.append(vstart + (vend - vstart) * cut)
537 privatedata.linebasepoints.append(graph.vpos_pt(*cutvpos))
538 privatedata.linebasepoints.append(graph.vpos_pt(*sharedata.vpos))
539 else:
540 # sometimes cut beginning and end
541 cutfrom = 0
542 cutto = 1
543 for vstart, vend in zip(privatedata.lastvpos, sharedata.vpos):
544 newcutfrom = None
545 if vstart > 1:
546 if vend > 1:
547 break
548 # 1 = vstart + (vend - vstart) * cutfrom
549 try:
550 newcutfrom = (1 - vstart)/(vend - vstart)
551 except ArithmeticError:
552 break
553 if vstart < 0:
554 if vend < 0:
555 break
556 # 0 = vstart + (vend - vstart) * cutfrom
557 try:
558 newcutfrom = - vstart/(vend - vstart)
559 except ArithmeticError:
560 break
561 if newcutfrom is not None and newcutfrom > cutfrom:
562 cutfrom = newcutfrom
563 newcutto = None
564 if vend > 1:
565 # 1 = vstart + (vend - vstart) * cutto
566 try:
567 newcutto = (1 - vstart)/(vend - vstart)
568 except ArithmeticError:
569 break
570 if vend < 0:
571 # 0 = vstart + (vend - vstart) * cutto
572 try:
573 newcutto = - vstart/(vend - vstart)
574 except ArithmeticError:
575 break
576 if newcutto is not None and newcutto < cutto:
577 cutto = newcutto
578 else:
579 if cutfrom < cutto:
580 cutfromvpos = []
581 cuttovpos = []
582 for vstart, vend in zip(privatedata.lastvpos, sharedata.vpos):
583 cutfromvpos.append(vstart + (vend - vstart) * cutfrom)
584 cuttovpos.append(vstart + (vend - vstart) * cutto)
585 privatedata.linebasepoints.append(graph.vpos_pt(*cutfromvpos))
586 privatedata.linebasepoints.append(graph.vpos_pt(*cuttovpos))
587 self.addpointstopath(privatedata, sharedata)
588 privatedata.lastvpos = sharedata.vpos[:]
589 else:
590 if len(privatedata.linebasepoints) > 1:
591 self.addpointstopath(privatedata, sharedata)
592 privatedata.lastvpos = None
594 def donedrawpoints(self, privatedata, sharedata, graph):
595 if len(privatedata.linebasepoints) > 1:
596 self.addpointstopath(privatedata, sharedata)
597 if privatedata.lineattrs is not None and len(privatedata.path.path):
598 privatedata.linecanvas.stroke(privatedata.path)
600 def key_pt(self, privatedata, sharedata, graph, x_pt, y_pt, width_pt, height_pt):
601 if privatedata.lineattrs is not None:
602 graph.stroke(path.line_pt(x_pt, y_pt+0.5*height_pt, x_pt+width_pt, y_pt+0.5*height_pt), privatedata.lineattrs)
605 class errorbar(_style):
607 needsdata = ["vpos", "vposmissing", "vposavailable", "vposvalid", "vrange", "vrangemissing"]
609 defaulterrorbarattrs = []
611 def __init__(self, size=0.1*unit.v_cm,
612 errorbarattrs=[],
613 epsilon=1e-10):
614 self.size = size
615 self.errorbarattrs = errorbarattrs
616 self.epsilon = epsilon
618 def columns(self, privatedata, sharedata, graph, columns):
619 for i in sharedata.vposmissing:
620 if i in sharedata.vrangemissing:
621 raise ValueError("position and range for a graph dimension missing")
622 return []
624 def selectstyle(self, privatedata, sharedata, graph, selectindex, selecttotal):
625 privatedata.errorsize_pt = unit.topt(attr.selectattr(self.size, selectindex, selecttotal))
626 privatedata.errorbarattrs = attr.selectattrs(self.defaulterrorbarattrs + self.errorbarattrs, selectindex, selecttotal)
628 def initdrawpoints(self, privatedata, sharedata, graph):
629 if privatedata.errorbarattrs is not None:
630 privatedata.errorbarcanvas = graph.insert(canvas.canvas())
631 privatedata.errorbarcanvas.set(privatedata.errorbarattrs)
632 privatedata.dimensionlist = list(xrange(len(sharedata.vpos)))
634 def drawpoint(self, privatedata, sharedata, graph):
635 if privatedata.errorbarattrs is not None:
636 for i in privatedata.dimensionlist:
637 for j in privatedata.dimensionlist:
638 if (i != j and
639 (sharedata.vpos[j] is None or
640 sharedata.vpos[j] < -self.epsilon or
641 sharedata.vpos[j] > 1+self.epsilon)):
642 break
643 else:
644 if ((sharedata.vrange[i][0] is None and sharedata.vpos[i] is None) or
645 (sharedata.vrange[i][1] is None and sharedata.vpos[i] is None) or
646 (sharedata.vrange[i][0] is None and sharedata.vrange[i][1] is None)):
647 continue
648 vminpos = sharedata.vpos[:]
649 if sharedata.vrange[i][0] is not None:
650 vminpos[i] = sharedata.vrange[i][0]
651 mincap = 1
652 else:
653 mincap = 0
654 if vminpos[i] > 1+self.epsilon:
655 continue
656 if vminpos[i] < -self.epsilon:
657 vminpos[i] = 0
658 mincap = 0
659 vmaxpos = sharedata.vpos[:]
660 if sharedata.vrange[i][1] is not None:
661 vmaxpos[i] = sharedata.vrange[i][1]
662 maxcap = 1
663 else:
664 maxcap = 0
665 if vmaxpos[i] < -self.epsilon:
666 continue
667 if vmaxpos[i] > 1+self.epsilon:
668 vmaxpos[i] = 1
669 maxcap = 0
670 privatedata.errorbarcanvas.stroke(graph.vgeodesic(*(vminpos + vmaxpos)))
671 for j in privatedata.dimensionlist:
672 if i != j:
673 if mincap:
674 privatedata.errorbarcanvas.stroke(graph.vcap_pt(j, privatedata.errorsize_pt, *vminpos))
675 if maxcap:
676 privatedata.errorbarcanvas.stroke(graph.vcap_pt(j, privatedata.errorsize_pt, *vmaxpos))
679 class text(_styleneedingpointpos):
681 needsdata = ["vpos", "vposmissing", "vposvalid"]
683 defaulttextattrs = [textmodule.halign.center, textmodule.vshift.mathaxis]
685 def __init__(self, textdx=0*unit.v_cm, textdy=0.3*unit.v_cm, textattrs=[], **kwargs):
686 self.textdx = textdx
687 self.textdy = textdy
688 self.textattrs = textattrs
690 def columns(self, privatedata, sharedata, graph, columns):
691 if "text" not in columns:
692 raise ValueError("text missing")
693 return ["text"] + _styleneedingpointpos.columns(self, privatedata, sharedata, graph, columns)
695 def selectstyle(self, privatedata, sharedata, graph, selectindex, selecttotal):
696 if self.textattrs is not None:
697 privatedata.textattrs = attr.selectattrs(self.defaulttextattrs + self.textattrs, selectindex, selecttotal)
698 else:
699 privatedata.textattrs = None
701 def initdrawpoints(self, privatedata, sharedata, grap):
702 privatedata.textdx_pt = unit.topt(self.textdx)
703 privatedata.textdy_pt = unit.topt(self.textdy)
705 def drawpoint(self, privatedata, sharedata, graph):
706 if privatedata.textattrs is not None and sharedata.vposvalid:
707 x_pt, y_pt = graph.vpos_pt(*sharedata.vpos)
708 try:
709 text = str(sharedata.point["text"])
710 except:
711 pass
712 else:
713 graph.text_pt(x_pt + privatedata.textdx_pt, y_pt + privatedata.textdy_pt, text, privatedata.textattrs)
715 def key_pt(self, privatedata, sharedata, graph, x_pt, y_pt, width_pt, height_pt):
716 raise RuntimeError("Style currently doesn't provide a graph key")
719 class arrow(_styleneedingpointpos):
721 needsdata = ["vpos", "vposmissing", "vposvalid"]
723 defaultlineattrs = []
724 defaultarrowattrs = []
726 def __init__(self, linelength=0.25*unit.v_cm, arrowsize=0.15*unit.v_cm, lineattrs=[], arrowattrs=[], epsilon=1e-10):
727 self.linelength = linelength
728 self.arrowsize = arrowsize
729 self.lineattrs = lineattrs
730 self.arrowattrs = arrowattrs
731 self.epsilon = epsilon
733 def columns(self, privatedata, sharedata, graph, columns):
734 if len(graph.axesnames) != 2:
735 raise ValueError("arrow style restricted on two-dimensional graphs")
736 if "size" not in columns:
737 raise ValueError("size missing")
738 if "angle" not in columns:
739 raise ValueError("angle missing")
740 return ["size", "angle"] + _styleneedingpointpos.columns(self, privatedata, sharedata, graph, columns)
742 def selectstyle(self, privatedata, sharedata, graph, selectindex, selecttotal):
743 if self.lineattrs is not None:
744 privatedata.lineattrs = attr.selectattrs(self.defaultlineattrs + self.lineattrs, selectindex, selecttotal)
745 else:
746 privatedata.lineattrs = None
747 if self.arrowattrs is not None:
748 privatedata.arrowattrs = attr.selectattrs(self.defaultarrowattrs + self.arrowattrs, selectindex, selecttotal)
749 else:
750 privatedata.arrowattrs = None
752 def initdrawpoints(self, privatedata, sharedata, graph):
753 privatedata.arrowcanvas = graph.insert(canvas.canvas())
755 def drawpoint(self, privatedata, sharedata, graph):
756 if privatedata.lineattrs is not None and privatedata.arrowattrs is not None and sharedata.vposvalid:
757 linelength_pt = unit.topt(self.linelength)
758 x_pt, y_pt = graph.vpos_pt(*sharedata.vpos)
759 try:
760 angle = sharedata.point["angle"] + 0.0
761 size = sharedata.point["size"] + 0.0
762 except:
763 pass
764 else:
765 if sharedata.point["size"] > self.epsilon:
766 dx = math.cos(angle*math.pi/180)
767 dy = math.sin(angle*math.pi/180)
768 x1 = x_pt-0.5*dx*linelength_pt*size
769 y1 = y_pt-0.5*dy*linelength_pt*size
770 x2 = x_pt+0.5*dx*linelength_pt*size
771 y2 = y_pt+0.5*dy*linelength_pt*size
772 privatedata.arrowcanvas.stroke(path.line_pt(x1, y1, x2, y2), privatedata.lineattrs +
773 [deco.earrow(privatedata.arrowattrs, size=self.arrowsize*size)])
775 def key_pt(self, privatedata, sharedata, graph, x_pt, y_pt, width_pt, height_pt):
776 raise RuntimeError("Style currently doesn't provide a graph key")
779 class rect(_style):
781 needsdata = ["vrange", "vrangeminmissing", "vrangemaxmissing"]
783 def __init__(self, palette=color.palette.Gray):
784 self.palette = palette
786 def columns(self, privatedata, sharedata, graph, columns):
787 if len(graph.axesnames) != 2:
788 raise TypeError("arrow style restricted on two-dimensional graphs")
789 if "color" not in columns:
790 raise ValueError("color missing")
791 if len(sharedata.vrangeminmissing) + len(sharedata.vrangemaxmissing):
792 raise ValueError("range columns incomplete")
793 return ["color"]
795 def initdrawpoints(self, privatedata, sharedata, graph):
796 privatedata.rectcanvas = graph.insert(canvas.canvas())
797 privatedata.lastcolorvalue = None
799 def drawpoint(self, privatedata, sharedata, graph):
800 xvmin = sharedata.vrange[0][0]
801 xvmax = sharedata.vrange[0][1]
802 yvmin = sharedata.vrange[1][0]
803 yvmax = sharedata.vrange[1][1]
804 if (xvmin is not None and xvmin < 1 and
805 xvmax is not None and xvmax > 0 and
806 yvmin is not None and yvmin < 1 and
807 yvmax is not None and yvmax > 0):
808 if xvmin < 0:
809 xvmin = 0
810 elif xvmax > 1:
811 xvmax = 1
812 if yvmin < 0:
813 yvmin = 0
814 elif yvmax > 1:
815 yvmax = 1
816 p = graph.vgeodesic(xvmin, yvmin, xvmax, yvmin)
817 p.append(graph.vgeodesic_el(xvmax, yvmin, xvmax, yvmax))
818 p.append(graph.vgeodesic_el(xvmax, yvmax, xvmin, yvmax))
819 p.append(graph.vgeodesic_el(xvmin, yvmax, xvmin, yvmin))
820 p.append(path.closepath())
821 colorvalue = sharedata.point["color"]
822 try:
823 if colorvalue != privatedata.lastcolorvalue:
824 privatedata.rectcanvas.set([self.palette.getcolor(colorvalue)])
825 except:
826 pass
827 else:
828 privatedata.rectcanvas.fill(p)
830 def key_pt(self, privatedata, sharedata, graph, x_pt, y_pt, width_pt, height_pt):
831 raise RuntimeError("Style currently doesn't provide a graph key")
834 class barpos(_style):
836 providesdata = ["vpos", "vposmissing", "vposavailable", "vposvalid", "vbarrange", "barcolumns", "barvalueindex", "stackedbar"]
838 defaultfrompathattrs = []
840 def __init__(self, fromvalue=None, frompathattrs=[], subindex=0, subnames=None, epsilon=1e-10):
841 # NOTE subindex is a perspective for higher dimensional plots
842 # (just ignore it for the moment -- we don't even need to document about it)
843 self.fromvalue = fromvalue
844 self.frompathattrs = frompathattrs
845 self.subindex = subindex
846 self.subnames = subnames
847 self.epsilon = epsilon
849 def columns(self, privatedata, sharedata, graph, columns):
850 sharedata.barcolumns = []
851 sharedata.barvalueindex = None
852 for dimension, axisnames in enumerate(graph.axesnames):
853 found = 0
854 for axisname in axisnames:
855 if axisname in columns:
856 if sharedata.barvalueindex is not None:
857 raise ValueError("multiple values")
858 sharedata.barvalueindex = dimension
859 sharedata.barcolumns.append(axisname)
860 found += 1
861 if (axisname + "name") in columns:
862 sharedata.barcolumns.append(axisname + "name")
863 found += 1
864 if found > 1:
865 raise ValueError("multiple names")
866 if not found:
867 raise ValueError("value/name missing")
868 if sharedata.barvalueindex is None:
869 raise ValueError("missing value")
870 if self.subindex >= sharedata.barvalueindex:
871 privatedata.barpossubindex = self.subindex + 1
872 else:
873 privatedata.barpossubindex = self.subindex
874 sharedata.vposmissing = []
875 return sharedata.barcolumns
877 def selectstyle(self, privatedata, sharedata, graph, selectindex, selecttotal):
878 if selecttotal == 1:
879 if self.subnames is not None:
880 raise ValueError("subnames set for single-bar data")
881 privatedata.barpossubname = []
882 else:
883 if self.subnames is not None:
884 privatedata.barpossubname = [self.subnames[selectindex]]
885 else:
886 privatedata.barpossubname = [selectindex]
888 def adjustaxis(self, privatedata, sharedata, graph, column, data, index):
889 try:
890 i = sharedata.barcolumns.index(column)
891 except ValueError:
892 pass
893 else:
894 if i == sharedata.barvalueindex:
895 if self.fromvalue is not None:
896 graph.axes[sharedata.barcolumns[i]].adjustrange([self.fromvalue], None)
897 graph.axes[sharedata.barcolumns[i]].adjustrange(data, index)
898 else:
899 if i == privatedata.barpossubindex:
900 graph.axes[sharedata.barcolumns[i][:-4]].adjustrange(data, index, privatedata.barpossubname)
901 else:
902 graph.axes[sharedata.barcolumns[i][:-4]].adjustrange(data, index)
904 def initdrawpoints(self, privatedata, sharedata, graph):
905 sharedata.vpos = [None]*(len(sharedata.barcolumns))
906 sharedata.vbarrange = [[None for i in xrange(2)] for x in sharedata.barcolumns]
907 sharedata.stackedbar = sharedata.stackedbardraw = 0
909 if self.fromvalue is not None:
910 privatedata.vfromvalue = graph.axes[sharedata.barcolumns[sharedata.barvalueindex][0]].convert(self.fromvalue)
911 if privatedata.vfromvalue < 0:
912 privatedata.vfromvalue = 0
913 if privatedata.vfromvalue > 1:
914 privatedata.vfromvalue = 1
915 if self.frompathattrs is not None:
916 # TODO 2d only
917 graph.stroke(graph.axespos[sharedata.barcolumns[sharedata.barvalueindex][0]].vgridpath(privatedata.vfromvalue),
918 self.defaultfrompathattrs + self.frompathattrs)
919 else:
920 privatedata.vfromvalue = 0
922 def drawpoint(self, privatedata, sharedata, graph):
923 sharedata.vposavailable = sharedata.vposvalid = 1
924 for i, barname in enumerate(sharedata.barcolumns):
925 if i == sharedata.barvalueindex:
926 sharedata.vbarrange[i][0] = privatedata.vfromvalue
927 try:
928 sharedata.vpos[i] = sharedata.vbarrange[i][1] = graph.axes[barname].convert(sharedata.point[barname])
929 except (ArithmeticError, ValueError, TypeError):
930 sharedata.vpos[i] = sharedata.vbarrange[i][1] = None
931 else:
932 for j in xrange(2):
933 try:
934 if i == privatedata.barpossubindex:
935 sharedata.vbarrange[i][j] = graph.axes[barname[:-4]].convert(([sharedata.point[barname]] + privatedata.barpossubname + [j]))
936 else:
937 sharedata.vbarrange[i][j] = graph.axes[barname[:-4]].convert((sharedata.point[barname], j))
938 except (ArithmeticError, ValueError, TypeError):
939 sharedata.vbarrange[i][j] = None
940 try:
941 sharedata.vpos[i] = 0.5*(sharedata.vbarrange[i][0]+sharedata.vbarrange[i][1])
942 except (ArithmeticError, ValueError, TypeError):
943 sharedata.vpos[i] = None
944 if sharedata.vpos[i] is None:
945 sharedata.vposavailable = sharedata.vposvalid = 0
946 elif sharedata.vpos[i] < -self.epsilon or sharedata.vpos[i] > 1+self.epsilon:
947 sharedata.vposvalid = 0
949 registerdefaultprovider(barpos(), ["vbarrange", "barcolumns", "barvalueindex"])
952 class stackedbarpos(_style):
954 # provides no additional data, but needs some data (and modifies some of them)
955 needsdata = ["vbarrange", "barcolumns", "barvalueindex"]
957 def __init__(self, stackname, epsilon=1e-10):
958 self.stackname = stackname
959 self.epsilon = epsilon
961 def columns(self, privatedata, sharedata, graph, columns):
962 if self.stackname not in columns:
963 raise ValueError("stackname column missing")
964 return [self.stackname]
966 def adjustaxis(self, privatedata, sharedata, graph, column, data, index):
967 if column == self.stackname:
968 graph.axes[sharedata.barcolumns[sharedata.barvalueindex]].adjustrange(data, index)
970 def initdrawpoints(self, privatedata, sharedata, graph):
971 if sharedata.stackedbardraw: # do not count the start bar when not gets painted
972 sharedata.stackedbar += 1
974 def drawpoint(self, privatedata, sharedata, graph):
975 sharedata.vbarrange[sharedata.barvalueindex][0] = sharedata.vbarrange[sharedata.barvalueindex][1]
976 try:
977 sharedata.vpos[sharedata.barvalueindex] = sharedata.vbarrange[sharedata.barvalueindex][1] = graph.axes[sharedata.barcolumns[sharedata.barvalueindex]].convert(sharedata.point[self.stackname])
978 except (ArithmeticError, ValueError, TypeError):
979 sharedata.vpos[sharedata.barvalueindex] = sharedata.vbarrange[sharedata.barvalueindex][1] = None
980 sharedata.vposavailable = sharedata.vposvalid = 0
981 else:
982 if not sharedata.vposavailable or not sharedata.vposvalid:
983 sharedata.vposavailable = sharedata.vposvalid = 1
984 for v in sharedata.vpos:
985 if v is None:
986 sharedata.vposavailable = sharedata.vposvalid = 0
987 break
988 if v < -self.epsilon or v > 1+self.epsilon:
989 sharedata.vposvalid = 0
992 class bar(_style):
994 needsdata = ["vbarrange"]
996 defaultbarattrs = [color.palette.Rainbow, deco.stroked([color.gray.black])]
998 def __init__(self, barattrs=[]):
999 self.barattrs = barattrs
1001 def columns(self, privatedata, sharedata, graph, columns):
1002 if len(graph.axesnames) != 2:
1003 raise TypeError("bar style restricted on two-dimensional graphs")
1004 return []
1006 def selectstyle(self, privatedata, sharedata, graph, selectindex, selecttotal):
1007 if selecttotal > 1:
1008 privatedata.barattrs = attr.selectattrs(self.defaultbarattrs + self.barattrs, selectindex, selecttotal)
1009 else:
1010 privatedata.barattrs = self.defaultbarattrs + self.barattrs
1012 def initdrawpoints(self, privatedata, sharedata, graph):
1013 privatedata.rectcanvas = graph.insert(canvas.canvas())
1014 sharedata.stackedbardraw = 1
1015 privatedata.stackedbar = sharedata.stackedbar
1017 def drawpoint(self, privatedata, sharedata, graph):
1018 xvmin = sharedata.vbarrange[0][0]
1019 xvmax = sharedata.vbarrange[0][1]
1020 yvmin = sharedata.vbarrange[1][0]
1021 yvmax = sharedata.vbarrange[1][1]
1022 if (xvmin is not None and xvmin < 1 and
1023 xvmax is not None and xvmax > 0 and
1024 yvmin is not None and yvmin < 1 and
1025 yvmax is not None and yvmax > 0):
1026 if xvmin < 0:
1027 xvmin = 0
1028 elif xvmax > 1:
1029 xvmax = 1
1030 if yvmin < 0:
1031 yvmin = 0
1032 elif yvmax > 1:
1033 yvmax = 1
1034 p = graph.vgeodesic(xvmin, yvmin, xvmax, yvmin)
1035 p.append(graph.vgeodesic_el(xvmax, yvmin, xvmax, yvmax))
1036 p.append(graph.vgeodesic_el(xvmax, yvmax, xvmin, yvmax))
1037 p.append(graph.vgeodesic_el(xvmin, yvmax, xvmin, yvmin))
1038 p.append(path.closepath())
1039 privatedata.rectcanvas.fill(p, privatedata.barattrs)
1041 def key_pt(self, privatedata, sharedata, graph, x_pt, y_pt, width_pt, height_pt):
1042 selectindex = privatedata.stackedbar
1043 selecttotal = sharedata.stackedbar + 1
1044 graph.fill(path.rect_pt(x_pt + width_pt*selectindex/float(selecttotal), y_pt, width_pt/float(selecttotal), height_pt), privatedata.barattrs)