source: orange/Orange/OrangeWidgets/Data/OWPaintData.py @ 11748:467f952c108d

Revision 11748:467f952c108d, 39.1 KB checked in by blaz <blaz.zupan@…>, 6 months ago (diff)

Changes in headers, widget descriptions text.

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