source: orange/Orange/OrangeWidgets/Data/OWPaintData.py @ 11566:f6ea199ce29d

Revision 11566:f6ea199ce29d, 35.6 KB checked in by Martin Frlin <martin.frlin@…>, 11 months ago (diff)

Added selection deleting with delete key.

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