Example #1
0
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 + ')'
Example #3
0
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'
Example #4
0
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'
Example #5
0
        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)
        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)
Example #7
0
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')
Example #8
0
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()
Example #9
0
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')