source: orange/Orange/OrangeWidgets/Data/OWPaintData.py @ 11738:6fc96a528c93

Revision 11738:6fc96a528c93, 39.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Cleanup OWItemModels

RevLine 
[9044]1"""
2<name>Paint Data</name>
[11719]3<description>Paints the data on a 2D plane. Place individual data
4points or use brush to paint a larger data sets.</description>
[9044]5<contact>Ales Erjavec (ales.erjavec(@at@)fri.uni-lj.si)</contact>
6<priority>40</priority>
[11096]7<icon>icons/PaintData.svg</icon>
[9044]8"""
[11566]9import orange
[9044]10
[11567]11import copy
12
[11566]13from PyQt4 import QtCore
[9044]14
15from OWWidget import *
16from OWGraph import *
17import OWToolbars
18
[11738]19from OWItemModels import VariableListModel, PyListModel, ModelActionsWidget
[9044]20import OWColorPalette
21
22dir = OWToolbars.dir
23icon_magnet = os.path.join(dir, "magnet_64px.png")
24icon_jitter = os.path.join(dir, "jitter_64px.png")
25icon_brush = os.path.join(dir, "brush_64px.png")
26icon_put = os.path.join(dir, "put_64px.png")
27icon_select = os.path.join(dir, "select-transparent_42px.png")
28icon_lasso = os.path.join(dir, "lasso-transparent_42px.png")
29#icon_remove = os.path.join(dir, "remove.svg")
30
31
32class PaintDataGraph(OWGraph):
33    def setData(self, data, attr1, attr2):
[9077]34        """ Set the data to display.
35       
36        :param data: data
37        :param attr1: attr for X axis
38        :param attr2: attr for Y axis
39        """
[9044]40        OWGraph.setData(self, data)
41        self.data = data
42        self.attr1 = attr1
43        self.attr2 = attr2
44        self.updateGraph()
45       
[11570]46    def updateGraph(self, dataInterval=None):
[9044]47        if dataInterval:
48            start, end = dataInterval
49            data = self.data[start:end]
50        else:
51            self.removeDrawingCurves()
52            data = self.data
[11567]53
[9044]54        clsValues, hasCls = (self.data.domain.classVar.values, True) if self.data.domain.classVar else ([0], False)
55       
56        palette = ColorPaletteGenerator(len(clsValues))
57        for i, cls in enumerate(clsValues):
58            x = [float(ex[self.attr1]) for ex in data if ex.getclass() == cls]
59            y = [float(ex[self.attr2]) for ex in data if ex.getclass() == cls]
60            self.addCurve("data points", xData=x, yData=y, brushColor=palette[i], penColor=palette[i])
61        self.replot()
62       
63    def drawCanvas(self, painter):
64        OWGraph.drawCanvas(self, painter)
65        pixmap = getattr(self, "_tool_pixmap", None)
66        if pixmap:
67            painter.drawPixmap(0, 0, pixmap)
68       
[9077]69       
[9044]70class DataTool(QObject):
[9077]71    """ A base class for data tools that operate on PaintDataGraph
72    widget by installing itself as its event filter.
73     
[9044]74    """
75    cursor = Qt.ArrowCursor
76    class optionsWidget(QFrame):
[9077]77        """ An options (parameters) widget for the tool (this will
78        be put in the "Options" box in the main OWPaintData widget
79        when this tool is selected.
80       
81        """
[9044]82        def __init__(self, tool, parent=None):
83            QFrame.__init__(self, parent)
84            self.tool = tool
85           
86    def __init__(self, graph, parent=None):
87        QObject.__init__(self, parent)
88        self.setGraph(graph)
89       
90    def setGraph(self, graph):
[9077]91        """ Install this tool to operate on ``graph``. If another tool
92        is already operating on the graph it will first be removed.
93       
94        """
[9044]95        self.graph = graph
96        if graph:
[11570]97            installed = getattr(graph, "_data_tool_event_filter", None)
[9044]98            if installed:
99                self.graph.canvas().removeEventFilter(installed)
[9077]100                installed.removed()
[9044]101            self.graph.canvas().setMouseTracking(True)
102            self.graph.canvas().installEventFilter(self)
103            self.graph._data_tool_event_filter = self
104            self.graph._tool_pixmap = None
105            self.graph.setCursor(self.cursor)
106            self.graph.replot()
[9077]107            self.installed()
108           
109    def removed(self):
110        """ Called when the tool is removed from a graph.
111        """
112        pass
113   
114    def installed(self):
115        """ Called when the tool is installed on a graph.
116        """
[11566]117        pass
[9044]118       
119    def eventFilter(self, obj, event):
120        if event.type() == QEvent.MouseButtonPress:
121            return self.mousePressEvent(event)
122        elif event.type() == QEvent.MouseButtonRelease:
123            return self.mouseReleaseEvent(event)
124        elif event.type() == QEvent.MouseButtonDblClick:
125            return self.mouseDoubleClickEvent(event)
126        elif event.type() == QEvent.MouseMove:
127            return self.mouseMoveEvent(event)
128        elif event.type() == QEvent.Paint:
129            return self.paintEvent(event)
130        elif event.type() == QEvent.Leave:
131            return self.leaveEvent(event)
132        elif event.type() == QEvent.Enter:
133            return self.enterEvent(event)
[9077]134        return False
[9044]135   
[9077]136    # These are actually event filters (note the return values)
[9044]137    def paintEvent(self, event):
138        return False
139   
140    def mousePressEvent(self, event):
141        return False
142   
143    def mouseMoveEvent(self, event):
144        return False
145   
146    def mouseReleaseEvent(self, event):
147        return False
148   
149    def mouseDoubleClickEvent(self, event):
150        return False
151   
152    def enterEvent(self, event):
153        return False
154   
155    def leaveEvent(self, event):
156        return False
157   
158    def keyPressEvent(self, event):
159        return False
160   
161    def transform(self, point):
162        x, y = point.x(), point.y()
163        x = self.graph.transform(QwtPlot.xBottom, x)
164        y = self.graph.transform(QwtPlot.yLeft, x)
165        return QPoint(x, y)
166   
167    def invTransform(self, point):
168        x, y = point.x(), point.y()
169        x = self.graph.invTransform(QwtPlot.xBottom, x)
170        y = self.graph.invTransform(QwtPlot.yLeft, y)
171        return QPointF(x, y)
172   
173    def attributes(self):
174        return self.graph.attr1, self.graph.attr2
175   
176    def dataTransform(self, *args):
177        pass
178   
[9077]179   
[9044]180class GraphSelections(QObject):
181    def __init__(self, parent, movable=True, multipleSelection=False):
182        QObject.__init__(self, parent)
183        self.selection = []
184        self.movable = movable
185        self.multipleSelection = multipleSelection
186       
187        self._moving_index, self._moving_pos, self._selection_region = -1, QPointF(), (QPointF(), QPointF())
188       
189    def getPos(self, event):
190        graph = self.parent()
191        pos = event.pos()
192        x = graph.invTransform(QwtPlot.xBottom, pos.x())
193        y = graph.invTransform(QwtPlot.yLeft, pos.y())
194        return QPointF(x, y)
195   
196    def toPath(self, region):
197        path = QPainterPath()
198        if isinstance(region, QRectF) or isinstance(region, QRect):
199            path.addRect(rect.normalized())
200        elif isinstance(region, tuple):
201            path.addRect(QRectF(*region).normalized())
202        elif isinstance(region, list):
203            path.addPolygon(QPolygonF(region + [region[0]]))
204        return path
205           
206   
207    def addSelectionRegion(self, region):
208        self.selection.append(region)
209        self.emit(SIGNAL("selectionRegionAdded(int, QPainterPath)"), len(self.selection) - 1, self.toPath(region))
210       
211    def setSelectionRegion(self, index, region):
212        self.selection[index] = region
213        self.emit(SIGNAL("selectionRegionUpdated(int, QPainterPath)"), index, self.toPath(region))
214       
215    def clearSelection(self):
216        for i, region in enumerate(self.selection):
217            self.emit(SIGNAL("selectionRegionRemoved(int, QPainterPath)"), i, self.toPath(region))
218        self.selection = []
219       
220    def start(self, event):
221        pos = self.getPos(event)
222        index = self.regionAt(event)
223        if index == -1 or not self.movable:
[9077]224            if event.modifiers() & Qt.ControlModifier and self.multipleSelection:
[9044]225                self.addSelectionRegion((pos, pos))
226            else:
227                self.clearSelection()
228                self.addSelectionRegion((pos, pos))
229            self._moving_index = -1
230        else:
231            self._moving_index, self._moving_pos, self._selection_region = index, pos, self.selection[index]
[11570]232            self.emit(SIGNAL("selectionRegionMoveStarted(int, QPointF, QPainterPath)"),
233                      index, pos, self.toPath(self.selection[index]))
[9044]234        self.emit(SIGNAL("selectionGeometryChanged()"))
235   
236    def update(self, event):
237        pos = self.getPos(event)
238        index = self._moving_index
239        if index == -1:
240            self.selection[-1] = self.selection[-1][:-1] + (pos,)
[11570]241            self.emit(SIGNAL("selectionRegionUpdated(int, QPainterPath)"), len(self.selection) - 1,
242                      self.toPath(self.selection[-1]))
[9044]243        else:
244            diff = self._moving_pos - pos
245            self.selection[index] = tuple([p - diff for p in self._selection_region])
[11570]246            self.emit(SIGNAL("selectionRegionMoved(int, QPointF, QPainterPath)"),
247                      index, pos, self.toPath(self.selection[index]))
[9044]248           
249        self.emit(SIGNAL("selectionGeometryChanged()"))
250   
251    def end(self, event):
252        self.update(event)
253        if self._moving_index != -1:
[9077]254            self.emit(SIGNAL("selectionRegionMoveFinished(int, QPointF, QPainterPath)"), 
255                      self._moving_index, self.getPos(event),
256                      self.toPath(self.selection[self._moving_index]))
[9044]257        self._moving_index = -1
258                     
259    def regionAt(self, event):
260        pos = self.getPos(event)
261        for i, region in enumerate(self.selection):
262            if self.toPath(region).contains(pos):
263                return i
264        return -1
265       
266    def testSelection(self, data):
267        data = numpy.asarray(data)
268        path = QPainterPath()
269        for region in self.selection:
270            path = path.united(self.toPath(region))
271        def test(point):
272            return path.contains(QPointF(point[0], point[1]))
273        test = numpy.apply_along_axis(test, 1, data)
274        return test
275   
276    def __nonzero__(self):
277        return bool(self.selection)
278   
279    def __bool__(self):
280        return bool(self.selection)
281   
282    def path(self):
283        path = QPainterPath()
284        for region in self.selection:
285            path = path.united(self.toPath(region))
286        return path
287   
288    def qTransform(self):
289        graph = self.parent()
290        invTransform = graph.invTransform
291        e1 = graph.canvas().mapFrom(graph, QPoint(1, 0))
292        e2 = graph.canvas().mapFrom(graph, QPoint(0, 1))
293        e1x, e1y = e1.x(), e1.y()
294        e2x, e2y = e2.x(), e2.y()
295        sx = invTransform(QwtPlot.xBottom, 1) - invTransform(QwtPlot.xBottom, 0)
296        sy = invTransform(QwtPlot.yLeft, 1) - invTransform(QwtPlot.yLeft, 0)
297        dx = invTransform(QwtPlot.xBottom, 0)
298        dy = invTransform(QwtPlot.yLeft, 0)
299        return QTransform(sx, 0.0, 0.0, sy, dx, dy)
300   
[9077]301   
[9044]302class SelectTool(DataTool):
303    class optionsWidget(QFrame):
304        def __init__(self, tool, parent=None):
305            QFrame.__init__(self, parent)
306            self.tool = tool
307            layout = QHBoxLayout()
308            delete = QToolButton(self)
309            delete.pyqtConfigure(text="Delete", toolTip="Delete selected instances")
310            self.connect(delete, SIGNAL("clicked()"), self.tool.deleteSelected)
311           
312            layout.addWidget(delete)
[9077]313            layout.addStretch(10)
[9044]314            self.setLayout(layout)
315       
[9077]316    def __init__(self, graph, parent=None, graphSelection=None):
317        DataTool.__init__(self, graph, parent)
318        if graphSelection is None:
319            self.selection = GraphSelections(graph)
320        else:
321            self.selection = graphSelection
[9044]322           
323        self.pen = QPen(Qt.black, 1, Qt.DashDotLine)
324        self.pen.setCosmetic(True)
325        self.pen.setJoinStyle(Qt.RoundJoin)
326        self.pen.setCapStyle(Qt.RoundCap)
[11570]327        self.connect(self.selection, SIGNAL("selectionRegionMoveStarted(int, QPointF, QPainterPath)"),
328                     self.onMoveStarted)
329        self.connect(self.selection, SIGNAL("selectionRegionMoved(int, QPointF, QPainterPath)"),
330                     self.onMove)
331        self.connect(self.selection, SIGNAL("selectionRegionMoveFinished(int, QPointF, QPainterPath)"),
332                     self.onMoveFinished)
333        self.connect(self.selection, SIGNAL("selectionRegionUpdated(int, QPainterPath)"),
334                     self.invalidateMoveSelection)
[9077]335        self._validMoveSelection = False
336        self._moving = None
[9044]337       
338    def setGraph(self, graph):
339        DataTool.setGraph(self, graph)
340        if graph and hasattr(self, "selection"):
341            self.selection.setParent(graph)
342
[9077]343    def installed(self):
344        DataTool.installed(self)
345        self.invalidateMoveSelection()
346       
[9044]347    def paintEvent(self, event):
348        if self.selection:
349            pixmap = QPixmap(self.graph.canvas().size())
350            pixmap.fill(QColor(255, 255, 255, 0))
351            painter = QPainter(pixmap)
352            painter.setRenderHints(QPainter.Antialiasing)
353            inverted, singular = self.selection.qTransform().inverted()
354            painter.setPen(self.pen)
355           
356            painter.setTransform(inverted)
357            for region in self.selection.selection:
358                painter.drawPath(self.selection.toPath(region))
359            del painter
360            self.graph._tool_pixmap = pixmap
361        return False
362       
363    def mousePressEvent(self, event):
364        if event.button() == Qt.LeftButton:
365            self.selection.start(event)
366            self.graph.replot()
367        return True
368   
369    def mouseMoveEvent(self, event):
370        index = self.selection.regionAt(event)
371        if index != -1:
372            self.graph.canvas().setCursor(Qt.OpenHandCursor)
373        else:
374            self.graph.canvas().setCursor(self.graph._cursor)
375           
376        if event.buttons() & Qt.LeftButton:
377            self.selection.update(event)
378            self.graph.replot()
379        return True
380   
381    def mouseReleaseEvent(self, event):
382        if event.button() == Qt.LeftButton:
383            self.selection.end(event)
384            self.graph.replot()
385        return True
[9077]386   
387    def invalidateMoveSelection(self, *args):
388        self._validMoveSelection = False
389        self._moving = None
[9044]390       
391    def onMoveStarted(self, index, pos, path):
392        data = self.graph.data
393        attr1, attr2 = self.graph.attr1, self.graph.attr2
[9077]394        if not self._validMoveSelection:
395            self._moving = [(i, float(ex[attr1]), float(ex[attr2])) for i, ex in enumerate(data)]
396            self._moving = [(i, x, y) for i, x, y in self._moving if path.contains(QPointF(x, y))]
397            self._validMoveSelection = True
398        self._move_anchor = pos
[9044]399       
400    def onMove(self, index, pos, path):
401        data = self.graph.data
402        attr1, attr2 = self.graph.attr1, self.graph.attr2
403       
[9077]404        diff = pos - self._move_anchor
[9044]405        for i, x, y in self._moving:
406            ex = data[i]
407            ex[attr1] = x + diff.x()
408            ex[attr2] = y + diff.y()
409        self.graph.updateGraph()
[9077]410        self.emit(SIGNAL("editing()"))
411       
412    def onMoveFinished(self, index, pos, path):
413        self.onMove(index, pos, path)
414        diff = pos - self._move_anchor
415        self._moving = [(i, x + diff.x(), y + diff.y()) \
416                        for i, x, y in self._moving]
417       
418        self.emit(SIGNAL("editingFinished()"))
[9044]419       
420    def deleteSelected(self, *args):
421        data = self.graph.data
422        attr1, attr2 = self.graph.attr1, self.graph.attr2
423        path = self.selection.path()
424        selected = [i for i, ex in enumerate(data) if path.contains(QPointF(float(ex[attr1]) , float(ex[attr2])))]
425        for i in reversed(selected):
426            del data[i]
[9077]427        self.graph.updateGraph()
428        if selected:
429            self.emit(SIGNAL("editing()"))
430            self.emit(SIGNAL("editingFinished()"))
[11570]431
432
[9044]433class GraphLassoSelections(GraphSelections):
434    def start(self, event):
435        pos = self.getPos(event)
436        index = self.regionAt(event)
437        if index == -1:
438            self.clearSelection()
439            self.addSelectionRegion([pos])
440        else:
441            self._moving_index, self._moving_pos, self._selection_region = index, pos, self.selection[index]
[11570]442            self.emit(SIGNAL("selectionRegionMoveStarted(int, QPointF, QPainterPath)"),
443                      index, pos, self.toPath(self.selection[index]))
[9044]444        self.emit(SIGNAL("selectionGeometryChanged()"))
445       
446    def update(self, event):
447        pos = self.getPos(event)
448        index = self._moving_index
449        if index == -1:
450            self.selection[-1].append(pos)
[11570]451            self.emit(SIGNAL("selectionRegionUpdated(int, QPainterPath)"),
452                      len(self.selection) - 1, self.toPath(self.selection[-1]))
[9044]453        else:
454            diff = self._moving_pos - pos
455            self.selection[index] = [p - diff for p in self._selection_region]
[11570]456            self.emit(SIGNAL("selectionRegionMoved(int, QPointF, QPainterPath)"),
457                      index, pos, self.toPath(self.selection[index]))
[9044]458           
459        self.emit(SIGNAL("selectionGeometryChanged()"))
460       
461    def end(self, event):
462        self.update(event)
463        if self._moving_index != -1:
[9077]464            self.emit(SIGNAL("selectionRegionMoveFinished(int, QPointF, QPainterPath)"), 
465                      self._moving_index, self.getPos(event),
466                      self.toPath(self.selection[self._moving_index]))
[9044]467        self._moving_index = -1
468       
[9077]469       
[9044]470class LassoTool(SelectTool):
471    def __init__(self, graph, parent=None):
[9077]472        SelectTool.__init__(self, graph, parent, 
473                            graphSelection=GraphLassoSelections(graph))
474#        self.selection = GraphLassoSelections(graph)
475#        self.pen = QPen(Qt.black, 1, Qt.DashDotLine)
476#        self.pen.setCosmetic(True)
477#        self.pen.setJoinStyle(Qt.RoundJoin)
478#        self.pen.setCapStyle(Qt.RoundCap)
479#        self.connect(self.selection, SIGNAL("selectionRegionMoveStarted(int, QPointF, QPainterPath)"), self.onMoveStarted)
480#        self.connect(self.selection, SIGNAL("selectionRegionMoved(int, QPointF, QPainterPath)"), self.onMove)
481#        self.connect(self.selection, SIGNAL("selectionRegionMoveFinished(int, QPointF, QPainterPath)"), self.onMoveFinished)
482   
[9044]483   
484class ZoomTool(DataTool):
485    def __init__(self, graph, parent=None):
486        DataTool.__init__(self, graph, parent)
487       
488    def paintEvent(self, event):
489        return False
490   
491    def mousePressEvent(self, event):
492        return False
493   
494    def mouseMoveEvent(self, event):
495        return False
496   
497    def mouseReleaseEvent(self, event):
498        return False
499   
500    def mouseDoubleClickEvent(self, event):
501        return False
502   
503    def keyPressEvent(self, event):
504        return False
505   
[9077]506   
[9044]507class PutInstanceTool(DataTool):
508    cursor = Qt.CrossCursor
509    def mousePressEvent(self, event):
510        if event.buttons() & Qt.LeftButton:
511            coord = self.invTransform(event.pos())
512            val1, val2 = coord.x(), coord.y()
513            attr1, attr2 = self.attributes()
514            self.dataTransform(attr1, val1, attr2, val2)
[9077]515            self.emit(SIGNAL("editing()"))
516            self.emit(SIGNAL("editingFinished()"))
[9044]517        return True
518       
519    def dataTransform(self, attr1, val1, attr2, val2):
520        example = orange.Example(self.graph.data.domain)
521        example[attr1] = val1
522        example[attr2] = val2
523        example.setclass(self.graph.data.domain.classVar(self.graph.data.domain.classVar.baseValue))
524        self.graph.data.append(example)
525        self.graph.updateGraph(dataInterval=(-1, sys.maxint))
526       
[9077]527       
[9044]528class BrushTool(DataTool):
529    brushRadius = 20
530    density = 5
531    cursor = Qt.CrossCursor
532   
533    class optionsWidget(QFrame):
534        def __init__(self, tool, parent=None):
535            QFrame.__init__(self, parent)
536            self.tool = tool
537            layout = QFormLayout()
538            self.radiusSlider = QSlider(Qt.Horizontal)
539            self.radiusSlider.pyqtConfigure(minimum=10, maximum=30, value=self.tool.brushRadius)
540            self.densitySlider = QSlider(Qt.Horizontal)
541            self.densitySlider.pyqtConfigure(minimum=3, maximum=10, value=self.tool.density)
542           
543            layout.addRow("Radius", self.radiusSlider)
544            layout.addRow("Density", self.densitySlider)
545            self.setLayout(layout)
546           
547            self.connect(self.radiusSlider, SIGNAL("valueChanged(int)"),
548                         lambda value: setattr(self.tool, "brushRadius", value))
549           
550            self.connect(self.densitySlider, SIGNAL("valueChanged(int)"),
551                         lambda value: setattr(self.tool, "density", value))
552   
553    def __init__(self, graph, parent=None):
554        DataTool.__init__(self, graph, parent)
555        self.brushState = -20, -20, 0, 0
556   
557    def mousePressEvent(self, event):
558        self.brushState = event.pos().x(), event.pos().y(), self.brushRadius, self.brushRadius
559        x, y, rx, ry = self.brushGeometry(event.pos())
560        if event.buttons() & Qt.LeftButton:
561            attr1, attr2 = self.attributes()
562            self.dataTransform(attr1, x, rx, attr2, y, ry)
[9077]563            self.emit(SIGNAL("editing()"))
[9044]564        self.graph.replot()
565        return True
566       
567    def mouseMoveEvent(self, event):
568        self.brushState = event.pos().x(), event.pos().y(), self.brushRadius, self.brushRadius
569        x, y, rx, ry = self.brushGeometry(event.pos())
570        if event.buttons() & Qt.LeftButton:
571            attr1, attr2 = self.attributes()
572            self.dataTransform(attr1, x, rx, attr2, y, ry)
[9077]573            self.emit(SIGNAL("editing()"))
[9044]574        self.graph.replot()
575        return True
576   
577    def mouseReleaseEvent(self, event):
578        self.graph.replot()
[9077]579        if event.button() & Qt.LeftButton:
580            self.emit(SIGNAL("editingFinished()"))
[9044]581        return True
582   
583    def leaveEvent(self, event):
584        self.graph._tool_pixmap = None
585        self.graph.replot()
586        return False
587       
588    def paintEvent(self, event):
589        if not self.graph.canvas().underMouse():
590            self.graph._tool_pixmap = None
591            return False 
592           
593        pixmap = QPixmap(self.graph.canvas().size())
594        pixmap.fill(QColor(255, 255, 255, 0))
595        painter = QPainter(pixmap)
596        painter.setRenderHint(QPainter.Antialiasing)
597        try:
598            painter.setPen(QPen(Qt.black, 1))
599            x, y, w, h = self.brushState
600            painter.drawEllipse(QPoint(x, y), w, h)
601        except Exception, ex:
602            print ex
603        del painter
604        self.graph._tool_pixmap = pixmap
605        return False
606       
607    def brushGeometry(self, point):
608        coord = self.invTransform(point)
609        dcoord = self.invTransform(QPoint(point.x() + self.brushRadius, point.y() + self.brushRadius))
610        x, y = coord.x(), coord.y()
611        rx, ry = dcoord.x() - x, -(dcoord.y() - y)
612        return x, y, rx, ry
613   
614    def dataTransform(self, attr1, x, rx, attr2, y, ry):
615        import random
616        new = []
617        for i in range(self.density):
618            ex = orange.Example(self.graph.data.domain)
619            ex[attr1] = random.normalvariate(x, rx)
620            ex[attr2] = random.normalvariate(y, ry)
621            ex.setclass(self.graph.data.domain.classVar(self.graph.data.domain.classVar.baseValue))
622            new.append(ex)
623        self.graph.data.extend(new)
624        self.graph.updateGraph(dataInterval=(-len(new), sys.maxint))
625   
[9077]626   
[9044]627class MagnetTool(BrushTool):
628    cursor = Qt.ArrowCursor
629    def dataTransform(self, attr1, x, rx, attr2, y, ry):
630        for ex in self.graph.data:
631            x1, y1 = float(ex[attr1]), float(ex[attr2])
632            distsq = (x1 - x)**2 + (y1 - y)**2
633            dist = math.sqrt(distsq)
634            attraction = self.density / 100.0
635            advance = 0.005
636            dx = -(x1 - x)/dist * attraction / max(distsq, rx) * advance
637            dy = -(y1 - y)/dist * attraction / max(distsq, ry) * advance
638            ex[attr1] = x1 + dx
639            ex[attr2] = y1 + dy
640        self.graph.updateGraph()
641   
[9077]642   
[9044]643class JitterTool(BrushTool):
644    cursor = Qt.ArrowCursor
645    def dataTransform(self, attr1, x, rx, attr2, y, ry):
646        import random
647        for ex in self.graph.data:
648            x1, y1 = float(ex[attr1]), float(ex[attr2])
649            distsq = (x1 - x)**2 + (y1 - y)**2
650            dist = math.sqrt(distsq)
651            attraction = self.density / 100.0
652            advance = 0.005
653            dx = -(x1 - x)/dist * attraction / max(distsq, rx) * advance
654            dy = -(y1 - y)/dist * attraction / max(distsq, ry) * advance
[11570]655            ex[attr1] = x1 - random.normalvariate(0, dx)    # *self.density)
656            ex[attr2] = y1 - random.normalvariate(0, dy)    # *self.density)
[9044]657        self.graph.updateGraph()
658       
[9077]659       
[9044]660class EnumVariableModel(PyListModel):
661    def __init__(self, var, parent=None, **kwargs):
662        PyListModel.__init__(self, [], parent, **kwargs)
663        self.wrap(var.values)
664        self.colorPalette = OWColorPalette.ColorPaletteHSV(len(self))
665        self.connect(self, SIGNAL("columnsInserted(QModelIndex, int, int)"), self.updateColors)
666        self.connect(self, SIGNAL("columnsRemoved(QModelIndex, int, int)"), self.updateColors)
667
668    def __delitem__(self, index):
669        raise TypeErorr("Cannot delete EnumVariable value")
670   
671    def __delslice__(self, i, j):
672        raise TypeErorr("Cannot delete EnumVariable values")
673   
674    def __setitem__(self, index, item):
675        self._list[index] = str(item)
676       
677    def data(self, index, role=Qt.DisplayRole):
678        if role == Qt.DecorationRole:
679            i = index.row()
680            return QVariant(self.itemQIcon(i))
681        else:
682            return PyListModel.data(self, index, role)
683       
684    def updateColors(self, index, start, end):
685        self.colorPalette = OWColorPalette.ColorPaletteHSV(len(self))
686        self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(0), self.index(len(self) - 1))
687       
688    def itemQIcon(self, i):
689        pixmap = QPixmap(64, 64)
690        pixmap.fill(QColor(255, 255, 255, 0))
691        painter = QPainter(pixmap)
692        painter.setRenderHint(QPainter.Antialiasing, True)
693        painter.setBrush(self.colorPalette[i])
694        painter.drawEllipse(QRectF(15, 15, 39, 39))
695        painter.end()
696        return QIcon(pixmap)
697   
[9077]698   
[9044]699class OWPaintData(OWWidget):
700    TOOLS = [("Brush", "Create multiple instances", BrushTool,  icon_brush),
701             ("Put", "Put individual instances", PutInstanceTool, icon_put),
702             ("Select", "Select and move instances", SelectTool, icon_select),
703             ("Lasso", "Select and move instances", LassoTool, icon_lasso),
704             ("Jitter", "Jitter instances", JitterTool, icon_jitter),
705             ("Magnet", "Move (drag) multiple instances", MagnetTool, icon_magnet),
706             ("Zoom", "Zoom", ZoomTool, OWToolbars.dlg_zoom) #"GenerateDataZoomTool.png")
707             ]
[9077]708    settingsList = ["commitOnChange"]
[9044]709    def __init__(self, parent=None, signalManager=None, name="Data Generator"):
[11313]710        OWWidget.__init__(self, parent, signalManager, name)
[9044]711       
[9546]712        self.outputs = [("Data", ExampleTable)]
[9044]713       
714        self.addClassAsMeta = False
715        self.attributes = []
716        self.cov = []
[9077]717        self.commitOnChange = False
[9044]718       
719        self.loadSettings()
720       
[11570]721        self.variablesModel = VariableListModel([orange.FloatVariable(name) for name in ["X", "Y"]], self,
722                                                flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable)
[9044]723
724        self.classVariable = orange.EnumVariable("Class label", values=["Class 1", "Class 2"], baseValue=0)
[11567]725
[9044]726        w = OWGUI.widgetBox(self.controlArea, "Class Label")
727       
728        self.classValuesView = listView = QListView()
729        listView.setSelectionMode(QListView.SingleSelection)
730       
[11570]731        self.classValuesModel = EnumVariableModel(self.classVariable, self,
732                                                  flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled| Qt.ItemIsEditable)
[9044]733        self.classValuesModel.wrap(self.classVariable.values)
734       
735        listView.setModel(self.classValuesModel)
736        listView.selectionModel().select(self.classValuesModel.index(0), QItemSelectionModel.ClearAndSelect)
[11570]737        self.connect(listView.selectionModel(),
738                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
739                     self.onClassLabelSelection)
[9044]740        listView.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum)
741        w.layout().addWidget(listView)
742       
743        self.addClassLabel = addClassLabel = QAction("+", self)
744        addClassLabel.pyqtConfigure(toolTip="Add class label")#, icon=QIcon(icon_put))
745        self.connect(addClassLabel, SIGNAL("triggered()"), self.addNewClassLabel)
746       
747        self.removeClassLabel = removeClassLabel = QAction("-", self)
748        removeClassLabel.pyqtConfigure(toolTip="Remove class label")#, icon=QIcon(icon_remove))
749        self.connect(removeClassLabel, SIGNAL("triggered()"), self.removeSelectedClassLabel)
750       
751        actionsWidget =  ModelActionsWidget([addClassLabel, removeClassLabel], self)
752        actionsWidget.layout().addStretch(10)
753        actionsWidget.layout().setSpacing(1)
754       
755        w.layout().addWidget(actionsWidget)
756       
757        toolbox = OWGUI.widgetBox(self.controlArea, "Tools", orientation=QGridLayout())
758        self.toolActions = QActionGroup(self)
759        self.toolActions.setExclusive(True)
760        for i, (name, tooltip, tool, icon) in enumerate(self.TOOLS):
761            action = QAction(name, self)
762            action.setToolTip(tooltip)
763            action.setCheckable(True)
764            if os.path.exists(icon):
765                action.setIcon(QIcon(icon))
766            self.connect(action, SIGNAL("triggered()"), lambda tool=tool: self.onToolAction(tool))
767            button = QToolButton()
768            button.setDefaultAction(action)
769            button.setIconSize(QSize(24, 24))
770            button.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
771            button.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
772            toolbox.layout().addWidget(button, i / 3, i % 3)
773            self.toolActions.addAction(action)
774           
775        for column in range(3):
776            toolbox.layout().setColumnMinimumWidth(column, 10)
777            toolbox.layout().setColumnStretch(column, 1)
778           
779        self.optionsLayout = QStackedLayout()
780        self.toolsStackCache = {}
781        optionsbox = OWGUI.widgetBox(self.controlArea, "Options", orientation=self.optionsLayout)
[11672]782
783
784        ur = OWGUI.widgetBox(self.controlArea, "")
785        undo = QAction("Undo", self)
786        undo.pyqtConfigure(toolTip="Undo action")
787        undo.setShortcut("Ctrl+Z")
788        self.connect(undo, SIGNAL("triggered()"), self.undoAction)
789
790        redo = QAction("Redo", self)
791        redo.pyqtConfigure(toolTip="Redo action")
792        redo.setShortcut("Ctrl+Shift+Z")
793        self.connect(redo, SIGNAL("triggered()"), self.redoAction)
794
795        actionsWidget =  ModelActionsWidget([undo, redo], self)
796        actionsWidget.layout().addStretch(10)
797        actionsWidget.layout().setSpacing(1)
798
799        ur.layout().addWidget(actionsWidget)
800
[9077]801        OWGUI.rubber(self.controlArea)
802        box = OWGUI.widgetBox(self.controlArea, "Commit")
[9044]803       
[9077]804        cb = OWGUI.checkBox(box, self, "commitOnChange", "Commit on change",
805                            tooltip="Send the data on any change.",
806                            callback=self.commitIf,)
807        b = OWGUI.button(box, self, "Commit", 
808                         callback=self.commit, default=True)
809        OWGUI.setStopper(self, b, cb, "dataChangedFlag", callback=self.commit)
[9044]810       
811        self.graph = PaintDataGraph(self)
812        self.graph.setAxisScale(QwtPlot.xBottom, 0.0, 1.0)
813        self.graph.setAxisScale(QwtPlot.yLeft, 0.0, 1.0)
814        self.graph.setAttribute(Qt.WA_Hover, True)
815        self.mainArea.layout().addWidget(self.graph)
[11567]816
[9044]817        self.currentOptionsWidget = None
818        self.data = []
[9077]819        self.dataChangedFlag = False 
[9044]820        self.domain = None
821       
822        self.onDomainChanged()
823        self.toolActions.actions()[0].trigger()
[11567]824
825        self.dataHistory = [(orange.ExampleTable(self.domain), ["Class 1", "Class 2"])]
826        self.historyCounter = 0
827        self.updateHistoryBool = True
[9044]828       
829        self.resize(800, 600)
[11567]830
[9044]831    def addNewClassLabel(self):
832        i = 1
833        while True:
[11570]834            newlabel = "Class %i" % i
[9044]835            if newlabel not in self.classValuesModel:
836#                self.classValuesModel.append(newlabel)
837                break
838            i += 1
839        values = list(self.classValuesModel) + [newlabel]
840        newclass = orange.EnumVariable("Class label", values=values)
841        newdomain = orange.Domain(self.graph.data.domain.attributes, newclass)
842        newdata = orange.ExampleTable(newdomain)
843        for ex in self.graph.data:
844            newdata.append(orange.Example(newdomain, [ex[a] for a in ex.domain.attributes] + [str(ex.getclass())]))
845       
846        self.classVariable = newclass
847        self.classValuesModel.wrap(self.classVariable.values)
848       
849        self.graph.data = newdata
850        self.graph.updateGraph()
851       
852        newindex = self.classValuesModel.index(len(self.classValuesModel) - 1)
853        self.classValuesView.selectionModel().select(newindex, QItemSelectionModel.ClearAndSelect)
854       
855        self.removeClassLabel.setEnabled(len(self.classValuesModel) > 1)
[11567]856
857        self.updateHistory()
[9044]858       
[11567]859    def removeSelectedClassLabel(self, label=None):
[9044]860        index = self.selectedClassLabelIndex()
861        if index is not None and len(self.classValuesModel) > 1:
[11567]862            if not label:
863                label = self.classValuesModel[index]
[9044]864            examples = [ex for ex in self.graph.data if str(ex.getclass()) != label]
865           
866            values = [val for val in self.classValuesModel if val != label]
[11568]867
[9044]868            newclass = orange.EnumVariable("Class label", values=values)
869            newdomain = orange.Domain(self.graph.data.domain.attributes, newclass)
870            newdata = orange.ExampleTable(newdomain)
871            for ex in examples:
872                if ex[self.classVariable] != label and ex[self.classVariable] in values:
[11570]873                    newdata.append(orange.Example(newdomain, [ex[a] for a in ex.domain.attributes] +
874                                                             [str(ex.getclass())]))
[11567]875
[9044]876            self.classVariable = newclass
877            self.classValuesModel.wrap(self.classVariable.values)
878           
879            self.graph.data = newdata
880            self.graph.updateGraph()
881           
882            newindex = self.classValuesModel.index(max(0, index - 1))
883            self.classValuesView.selectionModel().select(newindex, QItemSelectionModel.ClearAndSelect)
884           
[11567]885            self.removeClassLabel.setEnabled(len(self.classValuesModel) > 1)
886
887            self.updateHistory()
[9044]888       
889    def selectedClassLabelIndex(self):
890        rows = [i.row() for i in self.classValuesView.selectionModel().selectedRows()]
891        if rows:
892            return rows[0]
893        else:
894            return None
895       
896    def onClassLabelSelection(self, selected, unselected):
897        index = self.selectedClassLabelIndex()
898        if index is not None:
899            self.classVariable.baseValue = index
900   
901    def onToolAction(self, tool):
902        self.setCurrentTool(tool)
903       
904    def setCurrentTool(self, tool):
905        if tool not in self.toolsStackCache:
906            newtool = tool(None, self)
907            option = newtool.optionsWidget(newtool, self)
908            self.optionsLayout.addWidget(option)
[9077]909#            self.connect(newtool, SIGNAL("dataChanged()"), self.graph.updateGraph)
910#            self.connect(newtool, SIGNAL("dataChanged()"), self.onDataChanged)
911            self.connect(newtool, SIGNAL("editing()"), self.onDataChanged)
912            self.connect(newtool, SIGNAL("editingFinished()"), self.commitIf)
[11567]913            self.connect(newtool, SIGNAL("editingFinished()"), self.updateHistory)
[9044]914            self.toolsStackCache[tool] = (newtool, option)
915       
916        self.currentTool, self.currentOptionsWidget = tool, option = self.toolsStackCache[tool]
917        self.optionsLayout.setCurrentWidget(option)
918        self.currentTool.setGraph(self.graph)
[11566]919
[11567]920    def updateHistory(self):
921        if not self.updateHistoryBool:
922            return
[11568]923        # if we start updating from previously undone actions, we cut off redos in our history
[11567]924        if not self.historyCounter == len(self.dataHistory)-1:
[11575]925            self.dataHistory = self.dataHistory[:self.historyCounter+1]
[11568]926        # append an update of labels and data
[11567]927        labels = list(self.classValuesModel)
928        self.dataHistory.append((copy.deepcopy(self.graph.data), labels))
[11568]929        # set the size of the data history stack and remove the oldest update if we go over the size
930        if len(self.dataHistory) > 100:
[11567]931            self.dataHistory.pop(0)
[11568]932            self.historyCounter -= 1
[11567]933        self.historyCounter += 1
934
935    def undoAction(self):
[11568]936        # check to see if we are at the end of the stack
[11567]937        if self.historyCounter > 0:
938            self.historyCounter -= 1
939            data, labels = self.dataHistory[self.historyCounter]
[11568]940            # we dont update history when undoing
[11567]941            self.updateHistoryBool = False
[11568]942            # check if we only need to update labels
[11567]943            if len(self.classValuesModel) > len(labels):
944                diff = set(self.classValuesModel) - set(labels)
945                self.removeSelectedClassLabel(label=diff.pop())
[11568]946            elif len(self.classValuesModel) < len(labels):
[11567]947                self.addNewClassLabel()
[11568]948            # if not, update data
949            else:
[11575]950                self.graph.data = copy.deepcopy(data)
[11568]951                self.graph.updateGraph()
[11567]952            self.updateHistoryBool = True
953
954    def redoAction(self):
955        if self.historyCounter < len(self.dataHistory)-1:
956            self.historyCounter += 1
957            data, labels = self.dataHistory[self.historyCounter]
958            self.updateHistoryBool = False
959            if len(self.classValuesModel) > len(labels):
960                diff = set(self.classValuesModel) - set(labels)
961                self.removeSelectedClassLabel(label=diff.pop())
[11568]962            elif len(self.classValuesModel) < len(labels):
[11567]963                self.addNewClassLabel()
[11568]964            else:
[11575]965                self.graph.data = copy.deepcopy(data)
[11568]966                self.graph.updateGraph()
[11567]967            self.updateHistoryBool = True
968
[9044]969    def onDomainChanged(self, *args):
970        if self.variablesModel:
971            self.domain = orange.Domain(list(self.variablesModel), self.classVariable)
972            if self.data:
973                self.data = orange.ExampleTable(self.domain, self.data)
974            else:
975                self.data = orange.ExampleTable(self.domain)
976            self.graph.setData(self.data, 0, 1)
[9077]977           
978    def onDataChanged(self):
979        self.dataChangedFlag = True
[11566]980
981    def keyPressEvent(self, event):
982        if event.key() == QtCore.Qt.Key_Delete and isinstance(self.currentTool, SelectTool):
983            self.currentTool.deleteSelected()
[9044]984   
[9077]985    def commitIf(self):
986        if self.commitOnChange and self.dataChangedFlag:
987            self.commit()
988        else:
989            self.dataChangedFlag = True
990           
[9044]991    def commit(self):
[9077]992        data = self.graph.data
993        values = set([str(ex.getclass()) for ex in data])
994        if len(values) == 1:
995            # Remove the useless class variable.
996            domain = orange.Domain(data.domain.attributes, None)
997            data = orange.ExampleTable(domain, data)
[9546]998        self.send("Data", data)
[11567]999
[9044]1000if __name__ == "__main__":
1001    app = QApplication(sys.argv)
1002    w = OWPaintData()
1003    w.show()
1004    app.exec_()
Note: See TracBrowser for help on using the repository browser.