Start ioscopy script from config file
[oscopy.git] / oscopy / figure.py
blobd108f198391dc1f534212b68d15d709e886b83e2
1 """ Figure handler
3 A figure consist of a list of up to 4 graphs, with a layout.
4 Signal list are passed directly to each graph.
5 Layout can be either all graphs stacked verticaly, horizontaly or in quad
7 Properties
8 signals read/write Dict of Signals
9 graphs read/write List of Graphs
10 layout read/write String representing the layout
12 Signals
13 None
15 Abbreviations
16 curgraph: alias to the current graph
17 gn: graph number
18 g: graph
19 sn: signal name
21 class:
23 Figure -- Handle a list of graphs
25 methods:
26 __init__(sigs)
27 Create a figure and eventually initialize a graphics with a signal list
29 add(sigs)
30 Add a grapth into the figure
32 delete(num)
33 Delete a graph
35 update(toupdate, todelete)
36 Update the signals of the figure
38 [get|set]_mode(mode)
39 Set the mode of the current graph
41 [get|set]_layout(layout)
42 Set the layout, either horiz, vert or quad
44 draw()
45 Overload of matplotlib.pyplot.figure.draw()
47 signals()
48 Return a list of the signals in all graphs
50 _key()
51 Handle keystrokes during plot
52 """
54 from matplotlib.pyplot import Figure as MplFig, Axes
55 from graphs import Graph, LinGraph
57 MAX_GRAPHS_PER_FIGURE = 4
59 class Figure(MplFig):
60 """ Manage figure and its layout
61 """
62 def __init__(self, sigs={}, fig=None):
63 """ Instanciate a Figure.
64 If a signal list is provided, add a graph with the signal list
65 By default, create an empty list of graph and set the layout to horiz
67 Parameters
68 ----------
69 sigs: dict of Signals
70 If provided, the function instanciate the Figure with one Graph and
71 insert the Signals
73 fig: Not used
75 Returns
76 -------
77 The figure instanciated and initialized
78 """
79 MplFig.__init__(self)
81 self._layout = "horiz"
82 self._MODES_NAMES_TO_OBJ = {"lin":LinGraph}
83 self._kid = None
84 # FIXME: Slow way... Surely there exist something faster
85 self._OBJ_TO_MODES_NAMES = {}
86 for k, v in self._MODES_NAMES_TO_OBJ.iteritems():
87 self._OBJ_TO_MODES_NAMES[v] = k
89 if not sigs:
90 return
91 elif isinstance(sigs, dict):
92 self.add(sigs)
93 else:
94 print sigs
95 assert 0, _("Bad type")
97 def add(self, sigs={}):
98 """ Add a graph into the figure and set it as current graph.
99 Up to four graphs can be plotted on the same figure.
100 Additionnal attemps are ignored.
101 By default, do nothing.
103 Parameter
104 ---------
105 sigs: dict of Signals
106 The list of Signals to add
108 Returns
109 -------
110 Nothing
112 if len(self.axes) > (MAX_GRAPHS_PER_FIGURE - 1):
113 assert 0, _("Maximum graph number reached")
115 # _graph_position use the current length of self.axes to compute
116 # the graph coordinates. So we add a fake element into the list
117 # and we remove it once the size is computed
118 key = 'Nothing'
119 a = Axes(self, (0, 0, 1, 1))
120 self._axstack.add(key, a)
121 gr = LinGraph(self, self._graph_position(len(self.axes) - 1),\
122 sigs, label=str(len(self.axes)))
123 self._axstack.remove(a)
124 # Allocate a free number up to MAX_GRAPH_PER_FIGURE
125 num = 0
126 while num < MAX_GRAPHS_PER_FIGURE:
127 if self._axstack.get(num) is None:
128 break
129 num = num + 1
130 ax = self._axstack.add(num, gr)
132 # Force layout refresh
133 self.set_layout(self._layout)
135 def delete(self, num=1):
136 """ Delete a graph from the figure
137 By default, delete the first graph.
139 Parameter
140 ---------
141 num: integer
142 The position of the graph to delete
144 Returns
145 -------
146 Nothing
148 if not isinstance(num, int):
149 assert 0, _("Bad graph number")
150 if len(self.axes) < 1 or num < 1 or num > len(self.axes):
151 assert 0, _("Bad graph number")
152 self._axstack.remove(self._axstack[num - 1][1][1])
153 # Force layout refresh
154 self.set_layout(self._layout)
156 def update(self, u, d):
157 """ Update the graphs
158 FIXME: This function is deprecated by the use of GObject event system
160 For each Graph in the Figure, replace updated signals in u, and remove
161 deleted signals in d
163 Parameters
164 ----------
165 u: Dict of Signals
166 List of updated Signals to replace in the graphs
168 d: Dict of Signals
169 List of deleted Signals to remove from the graphs
171 Returns
172 -------
173 Nothing
175 if not isinstance(u, dict):
176 assert 0, _("Bad type")
177 if not isinstance(d, dict):
178 assert 0, _("Bad type")
179 for g in self.axes:
180 ug = {}
181 dg = {}
182 for sn in g.signals():
183 if sn in u:
184 ug[sn] = u[sn]
185 elif sn in d:
186 dg[sn] = d[sn]
187 g.insert(ug)
188 g.remove(dg)
190 # def get_mode(self):
191 # """ Return the mode of the current graph"""
192 # FIXME: DISABLED AS ONLY ONE MODE CURRENTLY AVAILABLE
193 # return self._OBJ_TO_MODES_NAMES(self._current)
195 def set_mode(self, args):
196 """ Set the mode of the current graph
197 Replace the graph provided by a new one of other mode, i.e. copy the
198 Signals from it.
199 The graph is replaced in the Figure internal list of graphs
201 Parameters
202 ----------
203 args: tuple of (graph, gmode)
204 graph: graph
205 The graph to change
206 gmode: string
207 The new mode
209 Returns
210 -------
211 Nothing
213 # Currently this cannot be tested (only one mode available)
214 old_graph = args[0]
215 gmode = args[1]
216 if not self._graphs:
217 assert 0, _("No graph defined")
218 g = (self._MODES_NAMES_TO_OBJ(gmode))(old_graph)
219 self._graphs[self._graphs.index(old_graph)] = g
221 def get_layout(self):
222 """ Return the figure layout
224 Parameter
225 ---------
226 Nothing
228 Returns
229 -------
230 string
231 The name of the current layout
233 return self._layout
235 def set_layout(self, layout="quad"):
236 """ Set the layout of the figure, default is 'quad'
237 'horiz' : graphs are horizontaly aligned
238 'vert' : graphs are verticaly aligned
239 'quad' : graphs are 2 x 2 at maximum
240 Other values are ignored
242 Parameters
243 ----------
244 layout: string
245 One of ['horiz'|'vert'|'quad']. Default is 'quad'
247 Returns
248 -------
249 Nothing
251 # To change the layout: use ax.set_position
253 if layout == "horiz" or layout == "vert" or layout == "quad":
254 self._layout = layout
255 else:
256 assert 0, _("Bad layout")
258 for gn, g in enumerate(self.axes):
259 g.set_position(self._graph_position(gn))
261 def draw(self, canvas):
262 """ Draw the Figure
263 Wrapper to parent class function. Set also the key_press_event callback
265 Parameter
266 ---------
267 canvas: Canvas
268 Canvas to use to draw the figure
270 Returns
271 -------
272 tmp: Value returned by parent class function call
274 tmp = MplFig.draw(self, canvas)
275 if not self._kid:
276 self._kid = self.canvas.mpl_connect('key_press_event', self._key)
278 return tmp
280 def _key(self, event):
281 """ Handle key press event
282 1, 2: toggle vertical cursors #0 and #1
283 3, 4: toggle horizontal cursors #0 and #1
285 Parameter
286 ---------
287 event: Matplotlib Event
288 The event that triggered the call-back
290 Returns
291 -------
292 Nothing
294 if event.inaxes is None:
295 return
296 # Find graph
297 g = None
298 for g in self.axes:
299 if g == event.inaxes:
300 break
301 else:
302 g = None
303 if g is None:
304 return
305 # Set cursor for graph
306 if event.key == "1":
307 g.toggle_cursors("vert", 0, event.xdata)
308 elif event.key == "2":
309 g.toggle_cursors("vert", 1, event.xdata)
310 elif event.key == "3":
311 g.toggle_cursors("horiz", 0, event.ydata)
312 elif event.key == "4":
313 g.toggle_cursors("horiz", 1, event.ydata)
314 else:
315 return
316 event.canvas.draw()
318 def _graph_position(self, num):
319 """ Compute the position of the graph upon its number and the layout
321 Parameter
322 ---------
323 num: integer
324 The position of the graph in the list
326 Returns
327 -------
328 array of 4 floats
329 The coordinates of the graph
331 # 1 graph: x:0->1 y:0->1 dx=1 dy=1
332 # 3 graphs horiz: x:0->.333 y:0->1 dx=.333 dy=1
333 # 3 graphs quads: x:0->.5 y:0->.5 dx=.5 dy=.5
335 if self._layout == "horiz":
336 dx = 1
337 dy = 1.0 / len(self.axes)
338 num_to_xy = [[0, y] for y in xrange(len(self.axes))]
339 elif self._layout == "vert":
340 dx = 1.0 / len(self.axes)
341 dy = 1
342 num_to_xy = [[x, 0] for x in xrange(len(self.axes))]
343 elif self._layout == "quad":
344 dx = 0.5
345 dy = 0.5
346 num_to_xy = [[x, y] for y in xrange(2) for x in xrange(2)]
347 else:
348 assert 0, _("Bad layout")
350 pos_x = num_to_xy[num][0]
351 pos_y = num_to_xy[len(self.axes) - num - 1][1]
352 x1 = pos_x * dx + 0.15 * dx
353 y1 = pos_y * dy + 0.15 * dy
354 return [x1, y1, dx * 0.75, dy * 0.75]
356 @property
357 def signals(self):
358 """ Return the list of signals in all graphs
359 Generator function.
361 Parameter
362 ---------
363 Nothing
365 Returns
366 -------
367 A list of the signals contained in all graphs
369 for g in self.axes:
370 for sn in g.get_signals():
371 yield sn
373 @property
374 def graphs(self):
375 """ Return the graph list
377 Parameter
378 ---------
379 Nothing
381 Returns
382 -------
383 The list of graphs
385 return self.axes
387 layout = property(get_layout, set_layout)
388 # mode = property(get_mode, set_mode)