class MFunctionSum(FunctionSum): def __init__(self, data): FunctionSum.__init__(self) self.data = data for f in self.data: if f.func in registry and not f.id.startswith('-'): self.add(f.func, f.name) self.terms[-1].data = f elif not f.id.startswith('-'): print >> sys.stderr, "function '%s' not found." % f.func, registry self.connect('add-term', self.on_add_term) self.connect('remove-term', self.on_remove_term) def on_add_term(self, state, term): if hasattr(term, 'data') and term.data.id.startswith('-'): raise StopAction row = self.data.append(id=create_id(), func=term.function.name, name=term.name) term.data = self.data[row] state['term'] = term def undo_add_term(self, state): term = state['term'] term.data.id = '-' + term.data.id self.terms.remove(term) self.emit('remove-term', term) def redo_add_term(self, state): term = state['term'] self.terms.append(term) self.emit('add-term', term) term.data.id = term.data.id[1:] on_add_term = action_from_methods2('graph/add-function-term', on_add_term, undo_add_term, redo=redo_add_term) def on_remove_term(self, state, term): if hasattr(term, 'data') and term.data.id.startswith('-'): raise StopAction term.data.id = '-' + term.data.id state['term'] = term undo_remove_term = redo_add_term redo_remove_term = undo_add_term on_remove_term = action_from_methods2('graph/remove-function-term', on_remove_term, undo_remove_term, redo=redo_remove_term)
class Dataset(DrawWithStyle): def __init__(self, graph, ind): self.graph, self.ind = graph, ind self.data = self.graph.data.datasets[ind] DrawWithStyle.__init__(self, graph, self.data) self.worksheet = self.graph.project.items[self.data.worksheet] self.x, self.y = self.worksheet[self.data.x], self.worksheet[ self.data.y] self.x.connect('rename', self.on_x_rename) self.y.connect('rename', self.on_y_rename) self.xfrom, self.xto = -inf, inf # self.recalculate() def on_x_rename(self, oldname, name): self.data.x = name.encode('utf-8') def on_y_rename(self, oldname, name): self.data.y = name.encode('utf-8') def connect_signals(self): self.x.connect('data-changed', self.on_data_changed) self.y.connect('data-changed', self.on_data_changed) def disconnect_signals(self): self.x.disconnect('data-changed', self.on_data_changed) self.y.disconnect('data-changed', self.on_data_changed) def on_data_changed(self): self.recalculate() self.emit('modified', self) # def __repr__(self): # return '<Dataset %s (#%d in graph "%s"), (%s, %s, %s)>' % (self.id, self.graph.datasets.index(self), self.graph.name, # self.worksheet.name, self.x.name, self.y.name) def active_data(self): length = min(len(self.x), len(self.y)) x = asarray(self.x)[:length] y = asarray(self.y)[:length] ind = isfinite(x) & isfinite(y) & (self.xfrom <= x) & (x <= self.xto) return ind def recalculate(self): # length = min(len(self.x), len(self.y)) # x = asarray(self.x)[:length] # y = asarray(self.y)[:length] # ind = isfinite(x) & isfinite(y) & (self.xfrom <= x) & (x <= self.xto) # self.xx = x[ind] # self.yy = y[ind] self.xx = asarray(self.x) self.yy = asarray(self.y) def set_range(self, _state, range): _state['old'] = self.xfrom, self.xto self.xfrom, self.xto = range # self.recalculate() self.emit('modified', self) def undo_set_range(self, _state): self.xfrom, self.xto = _state['old'] # self.recalculate() self.emit('modified', self) def get_range(self): return self.xfrom, self.xto set_range = action_from_methods2('dataset-set-range', set_range, undo_set_range) range = property(get_range, set_range) def paint(self): # t = time.time() # for i in xrange(10): # xx, yy = self.graph.data_to_phys(self.xx, self.yy) # print >>sys.stderr, 'o', (t - time.time())*1000, # t = time.time() # for i in xrange(10): if not hasattr(self, 'xx'): self.recalculate() self.paint_lines(self.xx, self.yy) self.paint_symbols(self.xx, self.yy) # print >>sys.stderr, 'i', (t - time.time())*1000 id = wrap_attribute('id') # this is nescessary! see graph.remove def __eq__(self, other): return self.id == other.id def set_worksheet(self, ws): self.data.worksheet = ws.id def get_worksheet(self): return self.graph.project.items[self.data.worksheet] def __str__(self): return self.x.worksheet.name + ':' + self.y.name + '(' + self.x.name + ')'
class Item(HasSignals): """Base class for all items in a Project""" def __init__(self, project, name=None, parent=None, location=None): self.project = project action_list.disable() if location is None or isinstance(location, dict): # this is a new item, not present in the database # create an entry for it self.view, self.data, self.id = project._create( type(self), location) # we have to handle creation of the top folder as a special case # we cannot specify its parent when we create it! if hasattr(self, '_isroot') and self._isroot: parent = self # parent defaults to top-level folder # (XXX: should this be the current folder?) if parent is None: parent = self.project.top if name is None: name = self.create_name(parent) if not self.check_name(name, parent): raise NameError # enter ourselves in the project dictionary self.project.items[self.id] = self # initialize self.name = name self.parent = parent.id else: # this is an item already present in the database self.view, self.data, self.id = location # enter ourselves in the project dictionary self.project.items[self.id] = self action_list.enable() # We have to emit the signal at the end # so the signal handlers can access wrapped attributes. # We can't emit in project.add() self.project.emit('add-item', self) def check_name(self, name, parent): if not re.match('^[a-zA-Z]\w*$', name): return False if isinstance(parent, Folder) and name in [i.name for i in parent.contents()]: return False return True def create_name(self, parent): for i in xrange(sys.maxint): name = self.default_name_prefix + str(i) if self.check_name(name, parent): return name def set_parent(self, state, parent): state['new'], state['old'] = parent, self._parent oldparent = self._parent self._parent = parent self.parent.emit('modified') if isinstance(oldparent, Folder): oldparent.emit('modified') else: raise StopAction def undo_set_parent(self, state): self._parent = state['old'] if state['old'] != '': state['old'].emit('modified') state['new'].emit('modified') def redo_set_parent(self, state): self._parent = state['new'] if state['old'] != '': state['old'].emit('modified') state['new'].emit('modified') set_parent = action_from_methods2('object/set-parent', set_parent, undo_set_parent, redo=redo_set_parent) def get_parent(self): return self._parent parent = property(get_parent, set_parent) _parent = wrap_attribute('parent') def set_name(self, state, n): if not self.check_name(n, self.parent): raise StopAction state['new'], state['old'] = n, self._name self._name = n self.set_name_notify() def undo_set_name(self, state): self._name = state['old'] self.set_name_notify() def redo_set_name(self, state): self._name = state['new'] self.set_name_notify() def set_name_notify(self): self.emit('rename', self._name, item=self) if isinstance(self.parent, Folder): self.parent.emit('modified') set_name = action_from_methods2('object/rename', set_name, undo_set_name, redo=redo_set_name) def get_name(self): return self._name name = property(get_name, set_name) _name = wrap_attribute('name') def todict(self): import mk return mk.row_to_dict(self.view, self.data) default_name_prefix = 'item'
class Worksheet(Item, HasSignals): def __init__(self, project, name=None, parent=None, location=None): self.__attr = False Item.__init__(self, project, name, parent, location) self.columns = [] if location is not None: for i in range(len(self.data.columns)): if not self.data.columns[i].name.startswith('-'): self.columns.append(Column(self, i)) self.__attr = True record = None def move_column(self, state, src=None, dest=None): if src is None and dest is None: src, dest = state['columns'] else: if src==dest or dest<0 or src<0 or dest>=len(self.columns) or src>=len(self.columns): raise StopAction, False state['columns'] = (src, dest) for i in range(src, dest, cmp(dest,src)): self.swap_columns(i, i+cmp(dest, src), nocomm=True) self.emit('data-changed') def undo_move_column(self, state): dest, src = state['columns'] for i in range(src, dest, cmp(dest,src)): self.swap_columns(i, i+cmp(dest, src), nocomm=True) self.emit('data-changed') move_column = action_from_methods2('move column', move_column, undo_move_column) def swap_columns(self, state, i=None, j=None, nocomm=False): if i is None and j is None: i, j = state['columns'] else: if i==j or i<0 or j<0 or i>=len(self.columns) or j>=len(self.columns): raise StopAction, False state['columns'] = (i, j) # swap rows in database # columns[i], columns[j] = columns[j], columns[i] will not work # (at least with metakit 2.4.9.3), so we have to do this explicitly tmp = self.data.columns.append() self.data.columns[tmp] = self.data.columns[i] self.data.columns[i] = self.data.columns[j] self.data.columns[j] = self.data.columns[tmp] del self.data.columns[tmp] # swap column objects self.columns[i], self.columns[j] = self.columns[j], self.columns[i] self.columns[i].reload(i) self.columns[j].reload(j) if nocomm: raise StopAction, True self.emit('data-changed') return True swap_columns = action_from_methods2('worksheet/swap-columns', swap_columns, swap_columns) def evaluate(self, expression): if expression == '': return [] project = self.project worksheet = self class funnydict(dict): def __init__(self, recordkeys, *args, **kwds): dict.__init__(self, *args, **kwds) self.recordset = set() self.recordkeys = recordkeys def __getitem__(self, key): if key in self.recordkeys: self.recordset.add(key) return dict.__getitem__(self, key) namespace = funnydict((c.name for c in worksheet.columns)) namespace.update(arrays.__dict__) namespace['top'] = project.top namespace['here'] = project.this namespace['this'] = worksheet namespace['up'] = worksheet.parent.parent namespace.update(dict([(c.name, c) for c in worksheet.columns])) namespace.update(dict([(i.name, i) for i in worksheet.parent.contents()])) result = eval(expression, namespace) for name in namespace.recordset: self[name] return result def __getattr__(self, name): if name in self.column_names: return self[name] else: return object.__getattribute__(self, name) def __setattr__(self, name, value): if name.startswith('_') or hasattr(self.__class__, name) \ or name in self.__dict__ or not self.__attr: return object.__setattr__(self, name, value) else: if name not in self.column_names: self.add_column(name) self[name] = value def __delattr__(self, name): if name in self.column_names: self.remove_column(name) else: object.__delattr__(self, name) def column_index(self, name): return self.data.columns.select(*[{'name': n.encode('utf-8')} for n in self.column_names]).find(name=name.encode('utf-8')) def add_column(self, state, name): ind = self.data.columns.append(name=name.encode('utf-8'), id=create_id(), data='') self.columns.append(Column(self, ind)) self.emit('data-changed') state['obj'] = self.columns[-1] return name def add_column_undo(self, state): col = state['obj'] col.id = '-'+col.id self.columns.remove(col) self.emit('data-changed') def add_column_redo(self, state): col = state['obj'] col.id = col.id[1:] self.columns.append(col) self.emit('data-changed') add_column = action_from_methods2('worksheet/add_column', add_column, add_column_undo, redo=add_column_redo) def remove_column(self, state, name): ind = self.column_index(name) if ind == -1: raise NameError, "Worksheet does not have a column named %s" % name else: col = self.columns[ind] col.name = '-'+col.name del self.columns[ind] self.emit('data-changed') state['col'], state['ind'] = col, ind return (col, ind), None def undo_remove_column(self, state): col, ind = state['col'], state['ind'] col.name = col.name[1:] self.columns.insert(ind, col) self.emit('data-changed') remove_column = action_from_methods2('worksheet_remove_column', remove_column, undo_remove_column) def get_ncolumns(self): return len(self.columns) ncolumns = property(get_ncolumns) def get_nrows(self): try: return max([len(c) for c in self.columns]) except ValueError: return 0 nrows = property(get_nrows) def set_array(self, arr): if len(arr.shape) != 2: raise TypeError, "Array must be two-dimensional" for column in arr: name = self.suggest_column_name() self[name] = column def get_array(self): return array(self.columns) array = property(get_array, set_array) def __getitem__(self, key): if isinstance(key, int): return self.columns[key] elif isinstance(key, basestring) and key in self.column_names: column = self.columns[self.column_names.index(key)] if self.record is not None: self.record.add(column) return column else: raise IndexError def __setitem__(self, key, value): if isinstance(key, int): self.columns[key][:] = value elif isinstance(key, basestring): if key not in self.column_names: self.add_column(key) self.columns[self.column_names.index(key)][:] = value else: raise IndexError self.emit('data-changed') def __repr__(self): return '<Worksheet %s%s>' % (self.name, '(deleted)'*self.id.startswith('-')) def get_column_names(self): return [c.name for c in self.columns] column_names = property(get_column_names) def __iter__(self): for column in self.columns: yield column def suggest_column_name(self): def num_to_alpha(n): alphabet = 'abcdefghijklmnopqrstuvwxyz' name = '' n, ypol = n//len(alphabet), n%len(alphabet) if n == 0: return alphabet[ypol] name = num_to_alpha(n) + alphabet[ypol] return name i = 0 while num_to_alpha(i) in self.column_names: i+=1 return num_to_alpha(i) def export_ascii(self, f): for row in xrange(self.nrows): for col in xrange(self.ncolumns): f.write(str(self.columns[col][row])) f.write('\t') f.write('\n') default_name_prefix = 'sheet'
if expr != '': # set data without triggering a action MkArray.__setitem__(self, slice(None), data) self.worksheet.emit('data-changed') self.emit('data-changed') return True def undo_set_expr(self, state): self.do_set_expr(None, state['old'], setstate=False) if 'olddata' in state: MkArray.__setitem__(self, slice(None), state['olddata']) def redo_set_expr(self, state): self.do_set_expr(None, state['new'], setstate=False) set_expr = action_from_methods2('worksheet/column-expr', do_set_expr, undo_set_expr, redo=redo_set_expr) def get_expr(self): return self.data.expr.decode('utf-8') expr = property(get_expr, set_expr) def calculate(self): self[:] = self.worksheet.evaluate(self.expr) def set_id(self, id): self.data.id = id def get_id(self): return self.data.id id = property(get_id, set_id)
class Text(GraphObject): def __init__(self, graph, data): GraphObject.__init__(self, graph, data) self.handles.append(Handle(graph)) self.read_position() def draw(self): facesize = 12 * self.graph.magnification self.graph.textpainter.render_text(self.text, facesize, self.handles[0].x, self.handles[0].y, align_x='bottom', align_y='left') def get_text(self): return self.data.text.decode('utf-8') def set_text(self, state, value): state['old'] = self.data.text self.data.text = value.encode('utf-8') state['new'] = self.data.text self.emit('modified') self.graph.emit('redraw') def undo_set_text(self, state): self.data.text = state['old'] self.emit('modified') self.graph.emit('redraw') def redo_set_text(self, state): self.data.text = state['new'] self.emit('modified') self.graph.emit('redraw') set_text = action_from_methods2('graph/text/set-text', set_text, undo_set_text, redo=redo_set_text) text = property(get_text, set_text) def begin(self, x, y): self.handles[0].move(x, y) self.active_handle = self.handles[0] def get_x1(self): return self.handles[0].posx def set_x1(self, value): self.handles[0].posx = value self.emit('modified') self.graph.emit('redraw') _x1 = property(get_x1, set_x1) def get_y1(self): return self.handles[0].posy def set_y1(self, value): self.handles[0].posy = value self.emit('modified') self.graph.emit('redraw') _y1 = property(get_y1, set_y1) def hittest(self, x, y): h = self.handles[0].hittest(x, y) if h: self.dragstart = x, y else: self.dragstart = None return h id = wrap_attribute('id')
class GraphObject(HasSignals): """ The position of a graph object is completely defined by the position of one or more handles. When the user moves a handle it may be nescessary to move some of the others as well. """ def __init__(self, graph, location): self.graph, self.data = graph, location self.handles = [] self.active_handle = None self.dragstart = None def read_position(self): if self.data.position == '': self.data.position = '0%;0% 50%;50%' for hpos, handle in zip(self.data.position.split(' '), self.handles): handle.posx, handle.posy = hpos.split(';') def record_position(self, state): state['prev'] = self.data.position self.data.position = ' '.join(h.posx + ';' + h.posy for h in self.handles).encode('utf-8') state['pos'] = self.data.position self.emit('modified') def undo_record_position(self, state): self.data.position = state['prev'] self.read_position() self.graph.emit('redraw') self.emit('modified') def redo_record_position(self, state): self.data.position = state['pos'] self.read_position() self.graph.emit('redraw') self.emit('modified') record_position = action_from_methods2('graph/move-object', record_position, undo_record_position, redo=redo_record_position) def move_active_handle(self, x, y, record=True): """ Move the active handle to (x, y) """ if self.active_handle is None: return self.active_handle.move(x, y) if record: self.record_position() def nudge(self, x, y, record=True): for h in self.handles: h.move(h.x + x, h.y + y) if record: self.record_position() def draw(self): """ Draw the object, given the position of the handles """ raise NotImplementedError def hittest_handles(self, x, y): """ Tests if a point x, y is on a handle and sets active_handle """ for h in self.handles: if h.hittest(x, y): self.active_handle = h return True self.active_handle = None return False def hittest(self, x, y): """ Tests if a point x, y is on the object """ raise NotImplementedError def bounding_box(self): """ Returns the bounding box (xmin, ymin, xmax, ymax) of the object """ raise NotImplementedError def draw_handles(self): for h in self.handles: h.draw()
class Graph(Item, HasSignals): def __init__(self, project, name=None, parent=None, location=None): Item.__init__(self, project, name, parent, location) self.paint_xor_objects = False self.selected_datasets = [] self.mode = 'arrow' self.graph_objects = [] self.dragobj = None self.selected_object = None self.plot_height = 100 self.plot_width = 100 self.datasets = [] if location is not None: for i, l in enumerate(self.data.datasets): if not l.id.startswith('-'): self.datasets.append(Dataset(self, i)) self.datasets[-1].connect('modified', self.on_dataset_modified) for l in self.data.lines: if not l.id.startswith('-'): self.graph_objects.append(Line(self, l)) for l in self.data.text: if not l.id.startswith('-'): self.graph_objects.append(Text(self, l)) self.functions = [] # if location is not None: # for i in range(len(self.data.functions)): # if not self.data.functions[i].id.startswith('-'): # f = Function(self, i) # self.functions.append(f) # f.connect('modified', self.on_dataset_modified) # f.func.connect('modified', self.on_dataset_modified) self.ps = False self.axis_top = Axis('top', self) self.axis_bottom = Axis('bottom', self) self.axis_right = Axis('right', self) self.axis_left = Axis('left', self) self.axes = [ self.axis_top, self.axis_right, self.axis_bottom, self.axis_left ] self.grid_h = Grid('horizontal', self) self.grid_v = Grid('vertical', self) self.set_range(0.0, 100.5) if location is None: self.xmin, self.ymin = 0, 0 self.ymax, self.xmax = 10, 10 self.newf() if self.xtype == '': self._xtype = 'linear' if self.ytype == '': self._ytype = 'linear' self.selected_function = None self.rubberband = Rubberband(self) self.cross = Cross(self) self.rangehandle = Rangehandle(self) self.objects = [self.rubberband, self.cross, self.rangehandle] self.textpainter = TextPainter(self) self.axis_title_font_size = 12. self.background_color = (1., 1., 1., 1.) self.pwidth = 120. self.pheight = 100. self.recalc = True default_name_prefix = 'graph' def redraw(self, recalc=False): if recalc: self.recalc = True self.emit('redraw') def get_xmin(self): try: return float(self._zoom.split()[0]) except IndexError: return 0.0 def get_xmax(self): try: return float(self._zoom.split()[1]) except IndexError: return 1.0 def get_ymin(self): try: return float(self._zoom.split()[2]) except IndexError: return 0.0 def get_ymax(self): try: return float(self._zoom.split()[3]) except IndexError: return 1.0 def set_xmin(self, value): self._zoom = ' '.join( [str(f) for f in [value, self.xmax, self.ymin, self.ymax]]) def set_xmax(self, value): self._zoom = ' '.join( [str(f) for f in [self.xmin, value, self.ymin, self.ymax]]) def set_ymin(self, value): self._zoom = ' '.join( [str(f) for f in [self.xmin, self.xmax, value, self.ymax]]) def set_ymax(self, value): self._zoom = ' '.join( [str(f) for f in [self.xmin, self.xmax, self.ymin, value]]) xmin = property(get_xmin, set_xmin) xmax = property(get_xmax, set_xmax) ymin = property(get_ymin, set_ymin) ymax = property(get_ymax, set_ymax) # axis scales def set_xtype(self, _state, tp): if tp == 'log' and (self.xmin <= 0 or self.xmax <= 0): raise StopAction _state['old'] = self._xtype self._xtype = tp self.redraw(True) def undo_set_xtype(self, _state): self._xtype = _state['old'] self.redraw(True) set_xtype = action_from_methods2('graph-set-xaxis-scale', set_xtype, undo_set_xtype) def get_xtype(self): return self._xtype xtype = property(get_xtype, set_xtype) def set_ytype(self, _state, tp): if tp == 'log' and (self.xmin <= 0 or self.xmax <= 0): raise StopAction _state['old'] = self._ytype self._ytype = tp self.redraw(True) def undo_set_ytype(self, _state): self._ytype = _state['old'] self.redraw(True) set_ytype = action_from_methods2('graph-set-xaxis-scale', set_ytype, undo_set_ytype) def get_ytype(self): return self._ytype ytype = property(get_ytype, set_ytype) # titles def set_xtitle(self, state, title): state['old'], state['new'] = self._xtitle, title self._xtitle = title self.reshape() self.redraw() def undo_set_xtitle(self, state): self._xtitle = state['old'] self.reshape() self.redraw() def redo_set_xtitle(self, state): self._xtitle = state['new'] self.reshape() self.redraw() def get_xtitle(self): return self._xtitle set_xtitle = action_from_methods2('graph/set-xtitle', set_xtitle, undo_set_xtitle, redo=redo_set_xtitle) xtitle = property(get_xtitle, set_xtitle) def set_ytitle(self, state, title): state['old'], state['new'] = self._ytitle, title self._ytitle = title self.reshape() self.redraw() def undo_set_ytitle(self, state): self._ytitle = state['old'] self.reshape() self.redraw() def redo_set_ytitle(self, state): self._ytitle = state['new'] self.reshape() self.redraw() def get_ytitle(self): return self._ytitle set_ytitle = action_from_methods2('graph/set-ytitle', set_ytitle, undo_set_ytitle, redo=redo_set_ytitle) ytitle = property(get_ytitle, set_ytitle) def __repr__(self): return '<Graph %s%s>' % (self.name, '(deleted)' * self.id.startswith('-')) def newf(self): # ind = self.data.functions.append(id=create_id()) f = Function(self) f.connect('modified', self.on_dataset_modified) f.func.connect('modified', self.on_dataset_modified) f.func.connect('add-term', self.on_dataset_modified) f.func.connect('remove-term', self.on_dataset_modified) self.functions.append(f) self.emit('add-function', f) return f def create_legend(self): legend = self.new_object(Text) legend.text = '\n'.join('@%d@' % i + str(d) for i, d in enumerate(self.datasets)) # add and remove graph objects def new_object(self, state, typ): location = {Line: self.data.lines, Text: self.data.text}[typ] ind = location.append(id=create_id()) obj = typ(self, location[ind]) self.graph_objects.append(obj) state['obj'] = obj return obj def undo_new_object(self, state): obj = state['obj'] self.graph_objects.remove(obj) obj.id = '-' + obj.id self.redraw() def redo_new_object(self, state): obj = state['obj'] self.graph_objects.append(obj) location = {Line: self.data.lines, Text: self.data.text}[type(obj)] obj.id = obj.id[1:] self.redraw() new_object = action_from_methods2('graph/new-object', new_object, undo_new_object, redo=redo_new_object) def delete_object(self, state, obj): obj.id = '-' + obj.id self.graph_objects.remove(obj) state['obj'] = obj self.redraw() delete_object = action_from_methods2('graph/delete-object', delete_object, redo_new_object, redo=undo_new_object) # add and remove datasets def add(self, state, x, y): ind = self.data.datasets.append(worksheet=x.worksheet.id, id=create_id(), x=x.name.encode('utf-8'), y=y.name.encode('utf-8')) d = Dataset(self, ind) self.datasets.append(d) pos = len(self.datasets) - 1 # print 'added dataset, index %d, position %d' % (ind, pos) d.connect('modified', self.on_dataset_modified) d.connect_signals() self.on_dataset_modified(d) self.emit('add-dataset', d) state['obj'] = d return pos def undo_add(self, state): d = state['obj'] # print 'undoing addition of dataset, index %d, position %d' % (d.ind, pos) self.datasets.remove(d) d.disconnect_signals() d.disconnect('modified', self.on_dataset_modified) self.emit('remove-dataset', d) self.redraw(True) d.id = '-' + d.id # self.data.datasets.delete(d.ind) def redo_add(self, state): d = state['obj'] d.id = d.id[1:] self.datasets.append(d) d.connect('modified', self.on_dataset_modified) d.connect_signals() self.emit('add-dataset', d) self.redraw(True) add = action_from_methods2('graph_add_dataset', add, undo_add, redo=redo_add) def remove(self, dataset): # we can do this even if `dataset` is a different object # than the one in self.datasets, if they have the same id # (see Dataset.__eq__) # TODO: why bother? just keep the object itself in the state ind = self.datasets.index(dataset) print 'removing dataset, index %d, position %d' % (dataset.ind, ind) dataset.id = '-' + dataset.id self.datasets.remove(dataset) try: dataset.disconnect('modified', self.on_dataset_modified) except NameError: pass self.emit('remove-dataset', dataset) self.redraw(True) return (dataset.ind, ind), None def undo_remove(self, data): ind, pos = data print 'undoing removal of dataset, index %d, position %d' % (ind, pos) dataset = Dataset(self, ind) dataset.id = dataset.id[1:] self.on_dataset_modified(dataset) self.datasets.insert(pos, dataset) dataset.connect('modified', self.on_dataset_modified) self.emit('add-dataset', dataset) self.redraw(True) remove = action_from_methods('graph_remove_dataset', remove, undo_remove) def on_dataset_modified(self, d=None): self.redraw(True) def paint_axes(self): for a in self.axes: a.paint_frame() self.grid_h.paint() self.grid_v.paint() def pos2y(self, pos): if pos.endswith('%'): return float(pos[:-1]) * self.plot_height / 100., '%' elif pos.endswith('y'): return self.data_to_phys(self.ymin, float(pos[:-1]))[1], 'y' elif pos.endswith('mm'): return float(pos[:-2]), 'mm' else: return float(pos), 'mm' def pos2x(self, pos): if pos.endswith('%'): return float(pos[:-1]) * self.plot_width / 100., '%' elif pos.endswith('x'): return self.data_to_phys(float(pos[:-1]), self.xmin)[0], 'x' elif pos.endswith('mm'): return float(pos[:-2]), 'mm' else: return float(pos), 'mm' def x2pos(self, x, typ): if typ == '%': return str(x * 100. / self.plot_width) + '%' elif typ == 'x': return str(self.phys_to_data(x, 0)[0]) + 'x' elif typ == 'mm': return str(x) + 'mm' def y2pos(self, y, typ): if typ == '%': return str(y * 100. / self.plot_height) + '%' elif typ == 'y': return str(self.phys_to_data(0, y)[1]) + 'y' elif typ == 'mm': return str(y) + 'mm' def data_to_phys(self, x, y): """ Takes a point x,y in data coordinates and transforms to physical coordinates """ x, xmin, xmax = map(self.axis_bottom.transform, (x, self.xmin, self.xmax)) y, ymin, ymax = map(self.axis_left.transform, (y, self.ymin, self.ymax)) px = self.plot_width * (x - xmin) / (xmax - xmin) py = self.plot_height * (y - ymin) / (ymax - ymin) # bt = self.axis_bottom.transform # lt = self.axis_left.transform # # px = self.plot_width * (bt(x) - bt(self.xmin)) / (bt(self.xmax) - bt(self.xmin)) # py = self.plot_height * (bt(y) - bt(self.ymin)) / (bt(self.ymax) - bt(self.ymin)) return px, py def phys_to_data(self, x, y): """ Takes a point x,y in physical coordinates and transforms to data coordinates """ xmin, xmax = map(self.axis_bottom.transform, (self.xmin, self.xmax)) ymin, ymax = map(self.axis_left.transform, (self.ymin, self.ymax)) px = x * (xmax - xmin) / self.plot_width + xmin py = y * (ymax - ymin) / self.plot_height + ymin return self.axis_bottom.invtransform(px), self.axis_left.invtransform( py) def mouse_to_phys(self, xm, ym): x = (xm / self.res) - self.marginl y = ((self.height_pixels - ym) / self.res) - self.marginb return x, y def mouse_to_data(self, xm, ym): x, y = self.mouse_to_phys(xm, ym) return self.phys_to_data(x, y) def autoscale(self): if len(self.datasets): xmin = min(d.xx.min() for d in self.datasets) xmax = max(d.xx.max() for d in self.datasets) ymin = min(d.yy.min() for d in self.datasets) ymax = max(d.yy.max() for d in self.datasets) self.zoom(xmin, xmax, ymin, ymax) def set_range(self, fr, to): self.fr, self.to = fr, to ##################### # zoom action # ##################### def zoom_do(self, state, xmin, xmax, ymin, ymax): eps = 1e-24 state['old'] = (self.xmin, self.xmax, self.ymin, self.ymax) if abs(xmin - xmax) <= eps or abs(ymin - ymax) <= eps: return self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax state['new'] = (xmin, xmax, ymin, ymax) def zoom_redo(self, state): self.xmin, self.xmax, self.ymin, self.ymax = state['new'] self.reshape() self.redraw(True) def zoom_undo(self, state): self.xmin, self.xmax, self.ymin, self.ymax = state['old'] self.reshape() self.redraw(True) def zoom_combine(self, state, other): return False zoom = action_from_methods2('graph-zoom', zoom_do, zoom_undo, redo=zoom_redo, combine=zoom_combine) def zoomout(self, x1, x2, x3, x4): if x3 == x4: return x1, x2 a = (x2 - x1) / (x4 - x3) c = x1 - a * x3 f1 = a * x1 + c f2 = a * x2 + c return min(f1, f2), max(f1, f2) def init(self): glClearColor(*self.background_color) glClear(GL_COLOR_BUFFER_BIT) # enable transparency glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glDisable(GL_DEPTH_TEST) glShadeModel(GL_FLAT) # we need this to render pil fonts properly glPixelStorei(GL_UNPACK_ALIGNMENT, 1) glPixelStorei(GL_PACK_ALIGNMENT, 1) self.listno = glGenLists(1) def display(self, width=-1, height=-1): if not hasattr(self, 'listno'): return if width == -1 and height == -1: width, height = self.last_width, self.last_height else: self.last_width, self.last_height = width, height if not self.paint_xor_objects: if self.recalc: if not self.ps: for i, d in enumerate(self.datasets): glClearColor(*self.background_color) glClear(GL_COLOR_BUFFER_BIT) w, h, _, renderer = self.textpainter.render_text_chunk_symbol( str(i)) if renderer is not None: renderer((w / 2) / self.res, (h / 2) / self.res) i = wx.EmptyImage(w, h) data = glReadPixels(int(self.marginl * self.res), int(self.marginb * self.res), int(w), int(h), GL_RGB, GL_UNSIGNED_BYTE) i.SetData(data) d._legend_wxbitmap = i.ConvertToBitmap() self.emit('shape-changed') glDeleteLists(self.listno, 1) glNewList(self.listno, GL_COMPILE) glClearColor(*self.background_color) glClear(GL_COLOR_BUFFER_BIT) # set up clipping glClipPlane(GL_CLIP_PLANE0, [1, 0, 0, 0]) glClipPlane(GL_CLIP_PLANE1, [-1, 0, 0, self.plot_width]) glClipPlane(GL_CLIP_PLANE2, [0, 1, 0, 0]) glClipPlane(GL_CLIP_PLANE3, [0, -1, 0, self.plot_height]) if len(self.datasets): for plane in [ GL_CLIP_PLANE0, GL_CLIP_PLANE1, GL_CLIP_PLANE2, GL_CLIP_PLANE3 ]: glEnable(plane) for d in self.datasets: d.paint() for f in self.functions: f.paint() if len(self.datasets): for plane in [ GL_CLIP_PLANE0, GL_CLIP_PLANE1, GL_CLIP_PLANE2, GL_CLIP_PLANE3 ]: glDisable(plane) self.paint_axes() glEndList() self.recalc = False glCallList(self.listno) for axis in self.axes: axis.paint_text() axis.paint_title() for o in self.graph_objects: o.draw() if self.mode == 'arrow' and self.selected_object == o: o.draw_handles() else: glLogicOp(GL_XOR) glEnable(GL_COLOR_LOGIC_OP) for o in self.objects: o.redraw() glDisable(GL_COLOR_LOGIC_OP) def reshape(self, width=-1, height=-1): if not hasattr(self, 'listno'): return t = time.time() if width == -1 and height == -1: width, height = self.last_width, self.last_height else: self.last_width, self.last_height = width, height # aspect ratio (width/height) self.aspect = self.pwidth / self.pheight # resolution (in pixels/mm) self.res = min(width / self.pwidth, height / self.pheight) displaydpi = 100. self.displayres = displaydpi / 25.4 # 25.4 = mm/inch self.magnification = self.res / self.displayres # set width and height self.width_pixels, self.height_pixels = width, height self.width_mm = width / self.res self.height_mm = height / self.res # measure titles facesize = self.axis_title_font_size * self.magnification if self.xtitle != '': _, tith = self.textpainter.render_text(self.xtitle, facesize, 0, 0, measure_only=True) else: tith = 0 if self.ytitle != '': titw, _ = self.textpainter.render_text(self.ytitle, facesize, 0, 0, measure_only=True, orientation='v') else: titw = 0 # measure tick labels try: self.ticw = max( self.textpainter.render_text(self.axis_left.totex(y), facesize, 0, 0, measure_only=True)[0] for y in self.axis_left.tics(self.ymin, self.ymax)[0]) # :-) except ValueError: self.ticw = 0 try: self.tich = max( self.textpainter.render_text(self.axis_bottom.totex(x), facesize, 0, 0, measure_only=True)[1] for x in self.axis_bottom.tics(self.xmin, self.xmax)[0]) except ValueError: self.tich = 0 # set margins (units are in mm) self.marginb = tith + self.tich + self.axis_title_font_size * self.magnification / 2 + 2 self.margint = self.height_mm * 0.03 self.marginl = titw + self.ticw + self.axis_title_font_size * self.magnification / 2 + 2 self.marginr = self.width_mm * 0.03 if self.width_mm / self.height_mm > self.aspect: self.marginr += (self.width_mm - self.height_mm * self.aspect) / 2 self.marginl += (self.width_mm - self.height_mm * self.aspect) / 2 else: self.margint += (self.height_mm - self.width_mm / self.aspect) / 2 self.marginb += (self.height_mm - self.width_mm / self.aspect) / 2 self.plot_width = self.width_mm - self.marginl - self.marginr self.plot_height = self.height_mm - self.margint - self.marginb # resize the viewport glViewport(0, 0, int(width), int(height)) self.viewport = glGetIntegerv(GL_VIEWPORT) # set opengl projection matrix with the origin # at the bottom left corner # of the graph # and scale in mm glMatrixMode(GL_PROJECTION) glLoadIdentity() glTranslated(-1. + 2. * self.marginl / self.width_mm, -1. + 2. * self.marginb / self.height_mm, 0) glScaled(2. / self.width_mm, 2. / self.height_mm, 1) # print >>sys.stderr, 'R: ', time.time()-t, "seconds" def export_ascii(self, outfile): # mathtext is not rendered directly self.pstext = [] save = self.width_pixels, self.height_pixels self.reshape(self.pwidth * self.displayres, self.pheight * self.displayres) d = tempfile.mkdtemp() filename = self.name + '.eps' f = open(os.path.join(d, filename), 'wb') gl2ps_BeginPage("Title", "Producer", self.viewport, f, filename) self.ps = True self.recalc = True self.display() self.ps = False gl2ps_EndPage() f.close() self.reshape(*save) f = open(d + '/' + filename, 'rb') for line in f: if line == '%%EndProlog\n': # insert encoded mathtext fonts # at the end of the prolog type42 = [] type42.append(FONTFILE) for fn in ['r', 'ex', 'mi', 'sy', 'tt']: type42.append( os.path.join(DATADIR, 'data', 'fonts', 'bakoma-cm', 'cm%s10.ttf' % fn)) for font in type42: print >> outfile, "%%BeginFont: " + FT2Font( str(font)).postscript_name print >> outfile, encodeTTFasPS(font) print >> outfile, "%%EndFont" outfile.write(line) elif line == 'showpage\n': # insert mathtext chunks # at the end of the file outfile.write(''.join(self.pstext)) outfile.write(line) else: # copy lines outfile.write(line) f.close() def show(self): for d in self.datasets: if not hasattr(d, 'xx'): d.recalculate() def button_press(self, x, y, button=None): if self.mode == 'zoom': if button in (1, 3): self.paint_xor_objects = True self.pixx, self.pixy = x, y self.ix, self.iy = self.mouse_to_phys(x, y) self.rubberband.show(self.ix, self.iy, self.ix, self.iy) self.redraw() if button == 2: self.haha = True else: self.haha = False elif self.mode == 'hand': if self.selected_function is not None: self.selected_function.set_reg(False) self.selected_function.move(*self.mouse_to_data(x, y)) # self.emit('redraw') self._movefunc = DrawFunction(self, self.selected_function) self.objects.append(self._movefunc) self.paint_xor_objects = True self._movefunc.show(*self.mouse_to_data(x, y)) self.redraw() elif self.mode == 's-reader': self.paint_xor_objects = True self.cross.show(*self.mouse_to_phys(x, y)) self.redraw() self.emit('status-message', '%f, %f' % self.mouse_to_data(x, y)) elif self.mode == 'range': self.paint_xor_objects = True self.rangehandle.show(*self.mouse_to_phys(x, y)) self.redraw() elif self.mode == 'd-reader': qx, qy = self.mouse_to_data(x, y) distances = [] closest_ind = [] for d in self.datasets: dist = (d.xx - qx) * (d.xx - qx) + (d.yy - qy) * (d.yy - qy) arg = argmin(dist) closest_ind.append(arg) distances.append(dist[arg]) ind = argmin(distances) dataset = self.datasets[ind] x, y = dataset.xx[closest_ind[ind]], dataset.yy[closest_ind[ind]] self.paint_xor_objects = True self.cross.show(*self.data_to_phys(x, y)) self.redraw() self.emit('status-message', '%f, %f' % (x, y)) elif self.mode == 'arrow': if button == 1: x, y = self.mouse_to_phys(x, y) for o in self.graph_objects: if o.hittest(x, y): self.selected_object = o self.dragobj = o self.dragobj.rec = False self.dragobj_xor = Move(self.dragobj) self.objects.append(self.dragobj_xor) self.paint_xor_objects = True self.dragobj_xor.show(x, y) if o.hittest_handles(x, y): self.dragobj.dragstart = None break else: self.selected_object = None self.redraw() elif button == 3: self.emit('right-clicked', None) print >> sys.stderr, 'right-clicked', None elif self.mode in ('draw-line', 'draw-text'): xi, yi = self.mouse_to_phys(x, y) createobj = self.new_object({ 'draw-line': Line, 'draw-text': Text }[self.mode]) createobj.begin(xi, yi) self.dragobj = createobj self.dragobj_xor = Move(self.dragobj) self.objects.append(self.dragobj_xor) self.paint_xor_objects = True self.dragobj_xor.show(xi, yi) self.selected_object = createobj self.mode = 'arrow' self.redraw() self.emit('request-cursor', 'arrow') def button_doubleclick(self, x, y, button): if self.mode == 'arrow' and button == 1: x, y = self.mouse_to_phys(x, y) for o in self.graph_objects: if o.hittest_handles(x, y): self.emit('object-doubleclicked', o) o.emit('modified') break def button_release(self, x, y, button): if self.mode == 'zoom': if button == 2: self.autoscale() self.redraw(True) elif button == 1 or button == 3: self.rubberband.hide() self.redraw() self.paint_xor_objects = False zix, ziy = self.mouse_to_data(self.pixx, self.pixy) zfx, zfy = self.mouse_to_data(x, y) _xmin, _xmax = min(zix, zfx), max(zix, zfx) _ymin, _ymax = min(zfy, ziy), max(zfy, ziy) if button == 3: _xmin, _xmax = self.axis_bottom.transform( _xmin), self.axis_bottom.transform(_xmax) _ymin, _ymax = self.axis_left.transform( _ymin), self.axis_left.transform(_ymax) xmin, xmax = self.zoomout( self.axis_bottom.transform(self.xmin), self.axis_bottom.transform(self.xmax), _xmin, _xmax) ymin, ymax = self.zoomout( self.axis_left.transform(self.ymin), self.axis_left.transform(self.ymax), _ymin, _ymax) xmin, xmax = self.axis_bottom.invtransform( xmin), self.axis_bottom.invtransform(xmax) ymin, ymax = self.axis_left.invtransform( ymin), self.axis_left.invtransform(ymax) else: xmin, xmax, ymin, ymax = _xmin, _xmax, _ymin, _ymax self.zoom(xmin, xmax, ymin, ymax) self.reshape() self.redraw(True) elif self.mode == 'hand': if self.selected_function is not None: self.selected_function.set_reg(True) self.selected_function.move(*self.mouse_to_data(x, y)) del self.objects[-1] self.paint_xor_objects = False self.redraw(True) elif self.mode == 's-reader': self.cross.hide() self.redraw() self.paint_xor_objects = False elif self.mode == 'd-reader': self.cross.hide() self.redraw() self.paint_xor_objects = False elif self.mode == 'arrow': if button == 1: if self.dragobj is not None: self.dragobj.rec = True self.dragobj.record_position() self.dragobj = None self.dragobj_xor.hide() self.objects.remove(self.dragobj_xor) self.paint_xor_objects = False self.redraw() elif self.mode == 'range': if button is None: button = self.__button else: self.__button = button x, y = self.mouse_to_data(x, y) for d in self.selected_datasets: if button == 1: d.range = (x, d.range[1]) elif button == 3: d.range = (d.range[0], x) elif button == 2: d.range = (-inf, inf) self.rangehandle.hide() self.redraw() self.paint_xor_objects = False def button_motion(self, x, y, dragging): if self.mode == 'zoom' and dragging and hasattr(self, 'ix'): self.rubberband.move(self.ix, self.iy, *self.mouse_to_phys(x, y)) self.redraw() elif self.mode == 'range' and dragging: self.rangehandle.move(*self.mouse_to_phys(x, y)) self.redraw() # self.button_press(x, y) elif self.mode == 'hand' and dragging: if self.selected_function is not None: self.selected_function.move(*self.mouse_to_data(x, y)) self._movefunc.move(*self.mouse_to_data(x, y)) self.redraw() elif self.mode == 's-reader' and dragging: self.cross.move(*self.mouse_to_phys(x, y)) self.redraw() self.emit('status-message', '%f, %f' % self.mouse_to_data(x, y)) elif self.mode == 'd-reader' and dragging: qx, qy = self.mouse_to_data(x, y) distances = [] closest_ind = [] for d in self.datasets: dist = (d.xx - qx) * (d.xx - qx) + (d.yy - qy) * (d.yy - qy) arg = argmin(dist) closest_ind.append(arg) distances.append(dist[arg]) ind = argmin(distances) dataset = self.datasets[ind] x, y = dataset.xx[closest_ind[ind]], dataset.yy[closest_ind[ind]] self.cross.move(*self.data_to_phys(x, y)) self.redraw() self.emit('status-message', '%f, %f' % (x, y)) elif self.mode == 'arrow': if not hasattr(self, 'res'): # not initialized yet, do nothing return x, y = self.mouse_to_phys(x, y) if self.dragobj is not None: # drag a handle on an object self.dragobj_xor.move(x, y) self.redraw() self.emit('request-cursor', 'none') else: # look for handles for o in self.graph_objects: if o.hittest(x, y): self.emit('request-cursor', 'hand') break else: self.emit('request-cursor', 'arrow') def key_down(self, keycode): import wx if keycode == wx.WXK_DELETE and self.selected_object is not None: self.delete_object(self.selected_object) _xtype = wrap_attribute('xtype') _ytype = wrap_attribute('ytype') _xtitle = wrap_attribute('xtitle') _ytitle = wrap_attribute('ytitle') _zoom = wrap_attribute('zoom')