remove unnecessary imports
[oscopy/ivan.git] / oscopy / graphs / graph.py
blobb53c34ee6ff79acc0c3298659bf300cad3a7526e
1 """ Graph Handler
3 A graph consist of several signals that share the same abscisse,
4 and plotted according a to mode, which is currently scalar.
6 Signals are managed as a dict, where the key is the signal name.
8 In a graph, signals with a different sampling, but with the same abscisse
9 can be plotted toget_her.
11 Handle a cursor dict, for convenience limited to two horizontal
12 and two vertical, limit can be removed.
14 sn: signal name
15 s : signal
17 Class Graph -- Handle the representation of a list of signals
19 methods:
20 __init__(sigs)
21 Create a graph and fill it with the sigs
23 __str__()
24 Return a string with the list of signals, and abscisse name
26 insert(sigs)
27 Add signal list to the graph, set_ the abscisse name
29 remove(sigs)
30 Delete signals from the graph
32 plot()
33 Plot the graph
35 signals()
36 Return a list of the signal names
38 type()
39 Return a string with the type of graph, to be overloaded.
41 set_units()
42 Define the axis unit
44 set_scale()
45 Set plot axis scale (lin, logx, logy, loglog)
47 set_range()
48 Set plot axis range
49 """
51 from matplotlib.pyplot import Axes as mplAxes
52 from matplotlib import rc
53 from cursor import Cursor
55 factors_to_names = {-18: ("a", 'atto'), -15:("f", 'femto'),
56 -12:("p", 'pico'), -9:("n", 'nano'),
57 -6:("u", 'micro'), -3:("m", 'milli'),
58 0:("",'(no scaling)'), 3:("k",'kilo'),
59 6:("M", 'mega'), 9:("G",'giga'),
60 12:("T", 'Tera'), 15:("P",'peta'),
61 18:("E", 'exa'), -31416:('auto', '(auto)')}
63 abbrevs_to_factors = {'E':18, 'P':15, 'T':12, 'G':9, 'M':6, 'k':3, '':0,
64 'a':-18, 'f':-15, 'p':-12, 'n':-9, 'u':-6, 'm':-3,
65 'auto':-31416}
67 names_to_factors = {'exa':18, 'peta':15, 'tera':12, 'giga':9, 'mega':6,
68 'kilo':3, '(no scaling)':0, 'atto':-18, 'femto':-15,
69 'pico':-12, 'nano':-9, 'micro':-6, 'milli':-3,
70 '(auto)':-31416}
73 class Graph(mplAxes):
74 def __init__(self, fig, rect, sigs={}, **kwargs):
75 """ Create a graph
76 If signals are provided, fill in the graph otherwise the graph is empty
77 Signals are assumed to exist and to be valid
78 If first argument is a Graph, then copy things
79 """
80 mplAxes.__init__(self, fig, rect, **kwargs)
81 self._sigs = {}
83 if isinstance(sigs, Graph):
84 mysigs = {}
85 mysigs = sigs.sigs.copy()
86 self._xrange = sigs.xrange
87 self._yrange = sigs.yrange
88 self._cursors = {"horiz": [None, None], "vert": [None, None]}
89 self._txt = None
90 self._scale_factors = sigs.scale_factors
91 self._signals2lines = sigs._signals2lines.copy()
92 self.insert(mysigs)
93 else:
94 self._xaxis = ""
95 self._yaxis = ""
96 self._xunit = ""
97 self._yunit = ""
98 self._xrange = []
99 self._yrange = []
100 # Cursors values, only two horiz and two vert but can be changed
101 self._cursors = {"horiz":[None, None], "vert":[None, None]}
102 self._txt = None
103 self._scale_factors = [None, None]
104 self._signals2lines = {}
105 self.insert(sigs)
107 def __str__(self):
108 """ Return a string with the type and the signal list of the graph
110 a = "(" + self.type + ") "
111 for sn in self._sigs.keys():
112 a = a + sn + " "
113 return a
115 def insert(self, sigs={}):
116 """ Add a list of signals into the graph
117 The first signal to be added defines the abscisse.
118 The remaining signals to be added must have the same abscisse name,
119 otherwise they are ignored
121 rejected = {}
122 for sn, s in sigs.iteritems():
123 if not self._sigs:
124 # First signal, set_ the abscisse name and add signal
125 self._xaxis = s.ref.name
126 self._xunit = s.ref.unit
127 self._yaxis = "Signals" # To change
128 self._yunit = s.unit
129 self._sigs[sn] = s
130 self.set_unit((self._xunit, self._yunit))
131 fx, l = self._find_scale_factor("X")
132 fy, l = self._find_scale_factor("Y")
133 x = s.ref.data * pow(10, fx)
134 y = s.data * pow(10, fy)
135 line, = self.plot(x, y, label=sn)
136 self._signals2lines[sn] = line
137 self._draw_cursors()
138 self._print_cursors()
139 self.legend()
140 else:
141 if s.ref.name == self._xaxis and s.unit == self._yunit and\
142 not self._sigs.has_key(s.name):
143 # Add signal
144 self._sigs[sn] = s
145 fx, l = self._find_scale_factor("X")
146 fy, l = self._find_scale_factor("Y")
147 x = s.ref.data * pow(10, fx)
148 y = s.data * pow(10, fy)
149 line, = self.plot(x, y, label=sn)
150 self._signals2lines[sn] = line
151 self.legend()
152 else:
153 # Ignore signal
154 rejected[sn] = s
155 return rejected
157 def remove(self, sigs={}):
158 """ Delete signals from the graph
160 for sn in sigs.iterkeys():
161 if sn in self._sigs.keys():
162 del self._sigs[sn]
163 self._signals2lines[sn].remove()
164 self.plot()
165 self.legend()
166 del self._signals2lines[sn]
167 return len(self._sigs)
169 def update_signals(self):
172 for sn, s in self._sigs.iteritems():
173 fx, l = self._find_scale_factor("X")
174 fy, l = self._find_scale_factor("Y")
175 x = s.ref.data * pow(10, fx)
176 y = s.data * pow(10, fy)
177 self._signals2lines[sn].set_xdata(x)
178 self._signals2lines[sn].set_ydata(y)
180 def _find_scale_factor(self, a):
181 """ Choose the right scale for data on axis a
182 Return the scale factor (f) and a string with the label. (l)
183 E.g. for data from 0.001 to 0.01 return 3 and "m" for milli-
185 if a == "X" and self._scale_factors[0] is not None:
186 return self._scale_factors[0], factors_to_names[-self._scale_factors[0]][0]
187 if a == "Y" and self._scale_factors[1] is not None:
188 return self._scale_factors[1], factors_to_names[-self._scale_factors[1]][0]
189 # Find the absolute maximum of the data
190 mxs = []
191 mns = []
193 for s in self._sigs.itervalues():
194 if a == "X":
195 mxs.append(max(s.ref.data))
196 mns.append(min(s.ref.data))
197 else:
198 mxs.append(max(s.data))
199 mns.append(min(s.data))
200 mx = abs(max(mxs))
201 mn = abs(min(mns))
202 mx = max(mx, mn)
204 # Find the scaling factor using the absolute maximum
205 if abs(mx) > 1:
206 fct = -3
207 else:
208 fct = 3
209 f = 0
210 while not (abs(mx * pow(10.0, f)) < 1000.0 \
211 and abs(mx * pow(10.0, f)) >= 1.0):
212 f = f + fct
213 if factors_to_names.has_key(-f) and \
214 ((self._xunit != "" and a == "X") or \
215 (self._yunit != "" and a != "X")):
216 l = factors_to_names[-f][0]
217 else:
218 if f == 0:
219 l = ""
220 else:
221 l = "10e" + str(-f) + " "
222 return f, l
224 def get_unit(self):
225 """ Return the graph units """
226 return self._xunit, self._yunit
228 def set_unit(self, unit):
229 """ Define the graph units. If only one argument is provided,
230 set y axis, if both are provided, set both.
232 if isinstance(unit, tuple):
233 if len(unit) == 1 or (len(unit) == 2 and not unit[1]):
234 self._yunit = unit[0]
235 elif len(unit) == 2 and unit[1]:
236 self._xunit = unit[0]
237 self._yunit = unit[1]
238 else:
239 assert 0, "Invalid argument"
240 else:
241 assert 0, "Invalid argument"
243 xl = self._xaxis
244 if not self._xunit:
245 xu = "a.u."
246 else:
247 xu = self._xunit
248 fx, l = self._find_scale_factor("X")
249 xl = xl + " (" + l + xu + ")"
250 yl = self._yaxis
251 if not self._yunit:
252 yu = "a.u."
253 else:
254 yu = self._yunit
255 fy, l = self._find_scale_factor("Y")
256 yl = yl + " (" + l + yu + ")"
257 mplAxes.set_xlabel(self, xl)
258 mplAxes.set_ylabel(self, yl)
261 def get_scale(self):
262 """ Return the axes scale
264 # return self._FUNC_TO_SCALES[self._plotf]
265 # Is there a better way?
266 x = self.get_xscale()
267 y = self.get_yscale()
268 if x == "linear" and y == "linear":
269 return "lin"
270 elif x == "linear" and y == "log":
271 return "logy"
272 elif y == "linear":
273 return "logx"
274 else:
275 return "loglog"
277 def set_scale(self, scale):
278 """ Set axes scale, either lin, logx, logy or loglog
280 SCALES_TO_STR = {"lin": ["linear", "linear"],\
281 "logx": ["log","linear"],\
282 "logy": ["linear", "log"],\
283 "loglog": ["log", "log"]}
284 mplAxes.set_xscale(self, SCALES_TO_STR[scale][0])
285 mplAxes.set_yscale(self, SCALES_TO_STR[scale][1])
287 def get_range(self):
288 """ Return the axes limits
290 self._xrange = mplAxes.get_xlim(self)
291 self._yrange = mplAxes.get_ylim(self)
292 return self._xrange, self._yrange
294 def set_range(self, arg="reset"):
295 """ Set axis range
296 Form 1: set_range("reset") delete range specs
297 Form 2: set_range(("x", [xmin, xmax])) set range for x axis
298 set_range(("y", [ymin, ymax])) set range for y axis
299 Form 3: set_range([xmin, xmax, ymin, ymax]) set range for both axis
301 if arg == "reset":
302 # Delete range specs
303 self._xrange = []
304 self._yrange = []
305 elif isinstance(arg, list) and len(arg) == 4:
306 self._xrange = [arg[0], arg[1]]
307 self._yrange = [arg[2], arg[3]]
308 elif isinstance(arg, tuple):
309 if len(arg) == 2 and isinstance(arg[0], str):
310 if arg[0] == "x" and isinstance(arg[1], list) and\
311 len(arg[1]) == 2:
312 self._xrange = arg[1]
313 elif arg[0] == "y" and isinstance(arg[1], list) and\
314 len(arg[1]) == 2:
315 self._yrange = arg[1]
316 else:
317 assert 0, "Unrecognized argument"
319 if len(self._xrange) == 2:
320 mplAxes.set_xlim(self, self._xrange[0], self._xrange[1])
321 if len(self._yrange) == 2:
322 mplAxes.set_ylim(self, self._yrange[0], self._yrange[1])
324 def toggle_cursors(self, ctype="", num=None, val=None):
325 """ Toggle the cursors in the graph
326 Call canvas.draw() shoud be called after to update the figure
327 cnt: cursor type
329 if not ctype or num is None or val is None:
330 return
332 if not ctype in ["horiz", "vert"]:
333 return
334 if num >= len(self._cursors[ctype]):
335 return
336 if val is None:
337 return
338 if self._cursors[ctype][num] is None:
339 self._set_cursor(ctype, num, val)
340 else:
341 self._cursors[ctype][num].value = val
342 self._cursors[ctype][num].set_visible()
343 self._cursors[ctype][num].draw(self)
344 self._print_cursors()
345 fx, lx = self._find_scale_factor("X")
346 fy, ly = self._find_scale_factor("Y")
348 def _draw_cursors(self):
349 """ Draw the cursor lines on the graph
350 Called at the end of plot()
352 fx, lx = self._find_scale_factor("X")
353 fy, ly = self._find_scale_factor("Y")
354 l = {"horiz": ly, "vert": lx}
355 txt = {"horiz": "", "vert": ""}
356 for t, ct in self._cursors.iteritems():
357 for c in ct:
358 if c is not None:
359 c.draw(self, ct.index(c))
361 def _set_cursor(self, ctype, num, val):
362 """ Add a cursor to the graph
364 if ctype in ["horiz", "vert"]:
365 if num >= 0 and num < 2:
366 # Just handle two cursor
367 self._cursors[ctype][num] = Cursor(val, ctype)
368 self._cursors[ctype][num].draw(self, num)
369 else:
370 assert 0, "Invalid cursor number"
371 else:
372 assert 0, "Invalid cursor type"
374 def _print_cursors(self):
375 """ Print cursors values on the graph
376 If both cursors are set_, print difference (delta)
378 fx, lx = self._find_scale_factor("X")
379 fy, ly = self._find_scale_factor("Y")
380 l = {"horiz": ly, "vert": lx}
381 u = {"horiz": self._yunit, "vert": self._xunit}
382 txt = {"horiz": "", "vert": ""}
383 # Preapre string for each cursor type (i.e. "horiz" and "vert")
384 for t, cl in self._cursors.iteritems():
385 for c in cl:
386 if c is not None and c.visible:
387 # Add cursors value to text
388 txt[t] += " %d: %8.3f %2s%-3s" \
389 % (cl.index(c) + 1, float(c.value), l[t], u[t])
390 if not None in cl and cl[0].visible and cl[1].visible:
391 # Add cursors difference (delta)
392 txt[t] += " d%s: %8.3f %2s%-3s"\
393 % (u[t], float(cl[1].value - cl[0].value),\
394 l[t], u[t])
396 if self._txt is None or not self._txt.axes == self:
397 # Add text to graph
398 rc('font', family='monospace')
399 self._txt = self.text(0.02, 0.1, "",\
400 transform=self.transAxes,\
401 axes=self)
402 self._txt.set_size(0.75 * self._txt.get_size())
403 # Update text
404 self._txt.set_text("%s\n%s" % (txt["horiz"], txt["vert"]))
406 @property
407 def signals(self):
408 """ Return a list of the signal names
410 return self._sigs
412 @property
413 def type(self):
414 """ Return a string with the type of the graph
415 To be overloaded by derived classes.
417 return
419 @property
420 def axis_names(self):
421 """Return the axis name"""
422 return [self._xaxis, self._yaxis]
424 def get_scale_factors(self):
425 """Return the scane factors names"""
426 fx, lx = self._find_scale_factor('X')
427 fy, ly = self._find_scale_factor('Y')
428 return [-fx, -fy]
430 def set_scale_factors(self, x_factor, y_factor):
431 old_fx, lx = self._find_scale_factor('X')
432 old_fy, ly = self._find_scale_factor('Y')
433 if x_factor is not None:
434 self._scale_factors[0] = -x_factor
435 else:
436 self._scale_factors[0] = x_factor
437 if y_factor is not None:
438 self._scale_factors[1] = -y_factor
439 else:
440 self._scale_factors[1] = y_factor
441 fx, lx = self._find_scale_factor('X')
442 fy, ly = self._find_scale_factor('Y')
443 xr, yr = self.get_range()
444 self.set_range([xr[0] * pow(10, fx - old_fx),
445 xr[1] * pow(10, fx - old_fx),
446 yr[0] * pow(10, fy - old_fy),
447 yr[1] * pow(10, fy - old_fy)])
448 self.update_signals()
449 self.set_unit((self._xunit, self._yunit))
451 unit = property(get_unit, set_unit)
452 scale = property(get_scale, set_scale)
453 range = property(get_range, set_range)
454 scale_factors = property(get_scale_factors, set_scale_factors)