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.

RevLine 
[11566]1import orange
[11567]2import copy
[11748]3import OWToolbars
4import OWColorPalette
5import Orange.data
[11566]6from PyQt4 import QtCore
[9044]7from OWWidget import *
8from OWGraph import *
[11748]9from OWItemModels import VariableListModel, PyListModel, ModelActionsWidget
[9044]10
[11748]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, )]
[9044]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):
[9077]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        """
[9044]39        OWGraph.setData(self, data)
40        self.data = data
41        self.attr1 = attr1
42        self.attr2 = attr2
43        self.updateGraph()
44       
[11570]45    def updateGraph(self, dataInterval=None):
[9044]46        if dataInterval:
47            start, end = dataInterval
48            data = self.data[start:end]
49        else:
50            self.removeDrawingCurves()
51            data = self.data
[11567]52
[9044]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       
[9077]68       
[9044]69class DataTool(QObject):
[9077]70    """ A base class for data tools that operate on PaintDataGraph
71    widget by installing itself as its event filter.
72     
[9044]73    """
74    cursor = Qt.ArrowCursor
75    class optionsWidget(QFrame):
[9077]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        """
[9044]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):
[9077]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        """
[9044]94        self.graph = graph
95        if graph:
[11570]96            installed = getattr(graph, "_data_tool_event_filter", None)
[9044]97            if installed:
98                self.graph.canvas().removeEventFilter(installed)
[9077]99                installed.removed()
[9044]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()
[9077]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        """
[11566]116        pass
[9044]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)
[9077]133        return False
[9044]134   
[9077]135    # These are actually event filters (note the return values)
[9044]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   
[9077]178   
[9044]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:
[9077]223            if event.modifiers() & Qt.ControlModifier and self.multipleSelection:
[9044]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]
[11570]231            self.emit(SIGNAL("selectionRegionMoveStarted(int, QPointF, QPainterPath)"),
232                      index, pos, self.toPath(self.selection[index]))
[9044]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,)
[11570]240            self.emit(SIGNAL("selectionRegionUpdated(int, QPainterPath)"), len(self.selection) - 1,
241                      self.toPath(self.selection[-1]))
[9044]242        else:
243            diff = self._moving_pos - pos
244            self.selection[index] = tuple([p - diff for p in self._selection_region])
[11570]245            self.emit(SIGNAL("selectionRegionMoved(int, QPointF, QPainterPath)"),
246                      index, pos, self.toPath(self.selection[index]))
[9044]247           
248        self.emit(SIGNAL("selectionGeometryChanged()"))
249   
250    def end(self, event):
251        self.update(event)
252        if self._moving_index != -1:
[9077]253            self.emit(SIGNAL("selectionRegionMoveFinished(int, QPointF, QPainterPath)"), 
254                      self._moving_index, self.getPos(event),
255                      self.toPath(self.selection[self._moving_index]))
[9044]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   
[9077]300   
[9044]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)
[9077]312            layout.addStretch(10)
[9044]313            self.setLayout(layout)
314       
[9077]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
[9044]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)
[11570]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)
[9077]334        self._validMoveSelection = False
335        self._moving = None
[9044]336       
337    def setGraph(self, graph):
338        DataTool.setGraph(self, graph)
339        if graph and hasattr(self, "selection"):
340            self.selection.setParent(graph)
341
[9077]342    def installed(self):
343        DataTool.installed(self)
344        self.invalidateMoveSelection()
345       
[9044]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
[9077]385   
386    def invalidateMoveSelection(self, *args):
387        self._validMoveSelection = False
388        self._moving = None
[9044]389       
390    def onMoveStarted(self, index, pos, path):
391        data = self.graph.data
392        attr1, attr2 = self.graph.attr1, self.graph.attr2
[9077]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
[9044]398       
399    def onMove(self, index, pos, path):
400        data = self.graph.data
401        attr1, attr2 = self.graph.attr1, self.graph.attr2
402       
[9077]403        diff = pos - self._move_anchor
[9044]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()
[9077]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()"))
[9044]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]
[9077]426        self.graph.updateGraph()
427        if selected:
428            self.emit(SIGNAL("editing()"))
429            self.emit(SIGNAL("editingFinished()"))
[11570]430
431
[9044]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]
[11570]441            self.emit(SIGNAL("selectionRegionMoveStarted(int, QPointF, QPainterPath)"),
442                      index, pos, self.toPath(self.selection[index]))
[9044]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)
[11570]450            self.emit(SIGNAL("selectionRegionUpdated(int, QPainterPath)"),
451                      len(self.selection) - 1, self.toPath(self.selection[-1]))
[9044]452        else:
453            diff = self._moving_pos - pos
454            self.selection[index] = [p - diff for p in self._selection_region]
[11570]455            self.emit(SIGNAL("selectionRegionMoved(int, QPointF, QPainterPath)"),
456                      index, pos, self.toPath(self.selection[index]))
[9044]457           
458        self.emit(SIGNAL("selectionGeometryChanged()"))
459       
460    def end(self, event):
461        self.update(event)
462        if self._moving_index != -1:
[9077]463            self.emit(SIGNAL("selectionRegionMoveFinished(int, QPointF, QPainterPath)"), 
464                      self._moving_index, self.getPos(event),
465                      self.toPath(self.selection[self._moving_index]))
[9044]466        self._moving_index = -1
467       
[9077]468       
[9044]469class LassoTool(SelectTool):
470    def __init__(self, graph, parent=None):
[9077]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   
[9044]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   
[9077]505   
[9044]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)
[9077]514            self.emit(SIGNAL("editing()"))
515            self.emit(SIGNAL("editingFinished()"))
[9044]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       
[9077]526       
[9044]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)
[9077]562            self.emit(SIGNAL("editing()"))
[9044]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)
[9077]572            self.emit(SIGNAL("editing()"))
[9044]573        self.graph.replot()
574        return True
575   
576    def mouseReleaseEvent(self, event):
577        self.graph.replot()
[9077]578        if event.button() & Qt.LeftButton:
579            self.emit(SIGNAL("editingFinished()"))
[9044]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   
[9077]625   
[9044]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   
[9077]641   
[9044]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
[11570]654            ex[attr1] = x1 - random.normalvariate(0, dx)    # *self.density)
655            ex[attr2] = y1 - random.normalvariate(0, dy)    # *self.density)
[9044]656        self.graph.updateGraph()
657       
[9077]658       
[9044]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   
[9077]697   
[9044]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             ]
[9077]707    settingsList = ["commitOnChange"]
[9044]708    def __init__(self, parent=None, signalManager=None, name="Data Generator"):
[11313]709        OWWidget.__init__(self, parent, signalManager, name)
[9044]710       
[9546]711        self.outputs = [("Data", ExampleTable)]
[9044]712       
713        self.addClassAsMeta = False
714        self.attributes = []
715        self.cov = []
[9077]716        self.commitOnChange = False
[9044]717       
718        self.loadSettings()
719       
[11570]720        self.variablesModel = VariableListModel([orange.FloatVariable(name) for name in ["X", "Y"]], self,
721                                                flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable)
[9044]722
723        self.classVariable = orange.EnumVariable("Class label", values=["Class 1", "Class 2"], baseValue=0)
[11567]724
[9044]725        w = OWGUI.widgetBox(self.controlArea, "Class Label")
726       
727        self.classValuesView = listView = QListView()
728        listView.setSelectionMode(QListView.SingleSelection)
729       
[11570]730        self.classValuesModel = EnumVariableModel(self.classVariable, self,
731                                                  flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled| Qt.ItemIsEditable)
[9044]732        self.classValuesModel.wrap(self.classVariable.values)
733       
734        listView.setModel(self.classValuesModel)
735        listView.selectionModel().select(self.classValuesModel.index(0), QItemSelectionModel.ClearAndSelect)
[11570]736        self.connect(listView.selectionModel(),
737                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
738                     self.onClassLabelSelection)
[9044]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)
[11672]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
[9077]800        OWGUI.rubber(self.controlArea)
801        box = OWGUI.widgetBox(self.controlArea, "Commit")
[9044]802       
[9077]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)
[9044]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)
[11567]815
[9044]816        self.currentOptionsWidget = None
817        self.data = []
[9077]818        self.dataChangedFlag = False 
[9044]819        self.domain = None
820       
821        self.onDomainChanged()
822        self.toolActions.actions()[0].trigger()
[11567]823
824        self.dataHistory = [(orange.ExampleTable(self.domain), ["Class 1", "Class 2"])]
825        self.historyCounter = 0
826        self.updateHistoryBool = True
[9044]827       
828        self.resize(800, 600)
[11567]829
[9044]830    def addNewClassLabel(self):
831        i = 1
832        while True:
[11570]833            newlabel = "Class %i" % i
[9044]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)
[11567]855
856        self.updateHistory()
[9044]857       
[11567]858    def removeSelectedClassLabel(self, label=None):
[9044]859        index = self.selectedClassLabelIndex()
860        if index is not None and len(self.classValuesModel) > 1:
[11567]861            if not label:
862                label = self.classValuesModel[index]
[9044]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]
[11568]866
[9044]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:
[11570]872                    newdata.append(orange.Example(newdomain, [ex[a] for a in ex.domain.attributes] +
873                                                             [str(ex.getclass())]))
[11567]874
[9044]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           
[11567]884            self.removeClassLabel.setEnabled(len(self.classValuesModel) > 1)
885
886            self.updateHistory()
[9044]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)
[9077]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)
[11567]912            self.connect(newtool, SIGNAL("editingFinished()"), self.updateHistory)
[9044]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)
[11566]918
[11567]919    def updateHistory(self):
920        if not self.updateHistoryBool:
921            return
[11568]922        # if we start updating from previously undone actions, we cut off redos in our history
[11567]923        if not self.historyCounter == len(self.dataHistory)-1:
[11575]924            self.dataHistory = self.dataHistory[:self.historyCounter+1]
[11568]925        # append an update of labels and data
[11567]926        labels = list(self.classValuesModel)
927        self.dataHistory.append((copy.deepcopy(self.graph.data), labels))
[11568]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:
[11567]930            self.dataHistory.pop(0)
[11568]931            self.historyCounter -= 1
[11567]932        self.historyCounter += 1
933
934    def undoAction(self):
[11568]935        # check to see if we are at the end of the stack
[11567]936        if self.historyCounter > 0:
937            self.historyCounter -= 1
938            data, labels = self.dataHistory[self.historyCounter]
[11568]939            # we dont update history when undoing
[11567]940            self.updateHistoryBool = False
[11568]941            # check if we only need to update labels
[11567]942            if len(self.classValuesModel) > len(labels):
943                diff = set(self.classValuesModel) - set(labels)
944                self.removeSelectedClassLabel(label=diff.pop())
[11568]945            elif len(self.classValuesModel) < len(labels):
[11567]946                self.addNewClassLabel()
[11568]947            # if not, update data
948            else:
[11575]949                self.graph.data = copy.deepcopy(data)
[11568]950                self.graph.updateGraph()
[11567]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())
[11568]961            elif len(self.classValuesModel) < len(labels):
[11567]962                self.addNewClassLabel()
[11568]963            else:
[11575]964                self.graph.data = copy.deepcopy(data)
[11568]965                self.graph.updateGraph()
[11567]966            self.updateHistoryBool = True
967
[9044]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)
[9077]976           
977    def onDataChanged(self):
978        self.dataChangedFlag = True
[11566]979
980    def keyPressEvent(self, event):
981        if event.key() == QtCore.Qt.Key_Delete and isinstance(self.currentTool, SelectTool):
982            self.currentTool.deleteSelected()
[9044]983   
[9077]984    def commitIf(self):
985        if self.commitOnChange and self.dataChangedFlag:
986            self.commit()
987        else:
988            self.dataChangedFlag = True
989           
[9044]990    def commit(self):
[9077]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)
[9546]997        self.send("Data", data)
[11567]998
[9044]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.