source: orange/Orange/OrangeWidgets/Data/OWPaintData.py @ 11567:53187432b22b

Revision 11567:53187432b22b, 38.4 KB checked in by Martin Frlin <martin.frlin@…>, 11 months ago (diff)

Added undo and redo but it is still buggy at the moment.

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
10import copy
11
12from PyQt4 import QtCore
13
14from OWWidget import *
15from OWGraph import *
16import OWToolbars
17
18from OWItemModels import VariableListModel, VariableDelegate, PyListModel, ModelActionsWidget
19import OWColorPalette
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)"), index, pos, self.toPath(self.selection[index])) 
232        self.emit(SIGNAL("selectionGeometryChanged()"))
233   
234    def update(self, event):
235        pos = self.getPos(event)
236        index = self._moving_index
237        if index == -1:
238            self.selection[-1] = self.selection[-1][:-1] + (pos,)
239            self.emit(SIGNAL("selectionRegionUpdated(int, QPainterPath)"), len(self.selection) - 1 , self.toPath(self.selection[-1]))
240        else:
241            diff = self._moving_pos - pos
242            self.selection[index] = tuple([p - diff for p in self._selection_region])
243            self.emit(SIGNAL("selectionRegionMoved(int, QPointF, QPainterPath)"), index, pos, self.toPath(self.selection[index]))
244           
245        self.emit(SIGNAL("selectionGeometryChanged()"))
246   
247    def end(self, event):
248        self.update(event)
249        if self._moving_index != -1:
250            self.emit(SIGNAL("selectionRegionMoveFinished(int, QPointF, QPainterPath)"), 
251                      self._moving_index, self.getPos(event),
252                      self.toPath(self.selection[self._moving_index]))
253        self._moving_index = -1
254                     
255    def regionAt(self, event):
256        pos = self.getPos(event)
257        for i, region in enumerate(self.selection):
258            if self.toPath(region).contains(pos):
259                return i
260        return -1
261       
262    def testSelection(self, data):
263        data = numpy.asarray(data)
264        path = QPainterPath()
265        for region in self.selection:
266            path = path.united(self.toPath(region))
267        def test(point):
268            return path.contains(QPointF(point[0], point[1]))
269        test = numpy.apply_along_axis(test, 1, data)
270        return test
271   
272    def __nonzero__(self):
273        return bool(self.selection)
274   
275    def __bool__(self):
276        return bool(self.selection)
277   
278    def path(self):
279        path = QPainterPath()
280        for region in self.selection:
281            path = path.united(self.toPath(region))
282        return path
283   
284    def qTransform(self):
285        graph = self.parent()
286        invTransform = graph.invTransform
287        e1 = graph.canvas().mapFrom(graph, QPoint(1, 0))
288        e2 = graph.canvas().mapFrom(graph, QPoint(0, 1))
289        e1x, e1y = e1.x(), e1.y()
290        e2x, e2y = e2.x(), e2.y()
291        sx = invTransform(QwtPlot.xBottom, 1) - invTransform(QwtPlot.xBottom, 0)
292        sy = invTransform(QwtPlot.yLeft, 1) - invTransform(QwtPlot.yLeft, 0)
293        dx = invTransform(QwtPlot.xBottom, 0)
294        dy = invTransform(QwtPlot.yLeft, 0)
295        return QTransform(sx, 0.0, 0.0, sy, dx, dy)
296   
297   
298class SelectTool(DataTool):
299    class optionsWidget(QFrame):
300        def __init__(self, tool, parent=None):
301            QFrame.__init__(self, parent)
302            self.tool = tool
303            layout = QHBoxLayout()
304            delete = QToolButton(self)
305            delete.pyqtConfigure(text="Delete", toolTip="Delete selected instances")
306            self.connect(delete, SIGNAL("clicked()"), self.tool.deleteSelected)
307           
308            layout.addWidget(delete)
309            layout.addStretch(10)
310            self.setLayout(layout)
311       
312    def __init__(self, graph, parent=None, graphSelection=None):
313        DataTool.__init__(self, graph, parent)
314        if graphSelection is None:
315            self.selection = GraphSelections(graph)
316        else:
317            self.selection = graphSelection
318           
319        self.pen = QPen(Qt.black, 1, Qt.DashDotLine)
320        self.pen.setCosmetic(True)
321        self.pen.setJoinStyle(Qt.RoundJoin)
322        self.pen.setCapStyle(Qt.RoundCap)
323        self.connect(self.selection, SIGNAL("selectionRegionMoveStarted(int, QPointF, QPainterPath)"), self.onMoveStarted)
324        self.connect(self.selection, SIGNAL("selectionRegionMoved(int, QPointF, QPainterPath)"), self.onMove)
325        self.connect(self.selection, SIGNAL("selectionRegionMoveFinished(int, QPointF, QPainterPath)"), self.onMoveFinished)
326        self.connect(self.selection, SIGNAL("selectionRegionUpdated(int, QPainterPath)"), self.invalidateMoveSelection)
327        self._validMoveSelection = False
328        self._moving = None
329       
330    def setGraph(self, graph):
331        DataTool.setGraph(self, graph)
332        if graph and hasattr(self, "selection"):
333            self.selection.setParent(graph)
334
335    def installed(self):
336        DataTool.installed(self)
337        self.invalidateMoveSelection()
338       
339    def paintEvent(self, event):
340        if self.selection:
341            pixmap = QPixmap(self.graph.canvas().size())
342            pixmap.fill(QColor(255, 255, 255, 0))
343            painter = QPainter(pixmap)
344            painter.setRenderHints(QPainter.Antialiasing)
345            inverted, singular = self.selection.qTransform().inverted()
346            painter.setPen(self.pen)
347           
348            painter.setTransform(inverted)
349            for region in self.selection.selection:
350                painter.drawPath(self.selection.toPath(region))
351            del painter
352            self.graph._tool_pixmap = pixmap
353        return False
354       
355    def mousePressEvent(self, event):
356        if event.button() == Qt.LeftButton:
357            self.selection.start(event)
358            self.graph.replot()
359        return True
360   
361    def mouseMoveEvent(self, event):
362        index = self.selection.regionAt(event)
363        if index != -1:
364            self.graph.canvas().setCursor(Qt.OpenHandCursor)
365        else:
366            self.graph.canvas().setCursor(self.graph._cursor)
367           
368        if event.buttons() & Qt.LeftButton:
369            self.selection.update(event)
370            self.graph.replot()
371        return True
372   
373    def mouseReleaseEvent(self, event):
374        if event.button() == Qt.LeftButton:
375            self.selection.end(event)
376            self.graph.replot()
377        return True
378   
379    def invalidateMoveSelection(self, *args):
380        self._validMoveSelection = False
381        self._moving = None
382       
383    def onMoveStarted(self, index, pos, path):
384        data = self.graph.data
385        attr1, attr2 = self.graph.attr1, self.graph.attr2
386        if not self._validMoveSelection:
387            self._moving = [(i, float(ex[attr1]), float(ex[attr2])) for i, ex in enumerate(data)]
388            self._moving = [(i, x, y) for i, x, y in self._moving if path.contains(QPointF(x, y))]
389            self._validMoveSelection = True
390        self._move_anchor = pos
391       
392    def onMove(self, index, pos, path):
393        data = self.graph.data
394        attr1, attr2 = self.graph.attr1, self.graph.attr2
395       
396        diff = pos - self._move_anchor
397        for i, x, y in self._moving:
398            ex = data[i]
399            ex[attr1] = x + diff.x()
400            ex[attr2] = y + diff.y()
401        self.graph.updateGraph()
402        self.emit(SIGNAL("editing()"))
403       
404    def onMoveFinished(self, index, pos, path):
405        self.onMove(index, pos, path)
406        diff = pos - self._move_anchor
407        self._moving = [(i, x + diff.x(), y + diff.y()) \
408                        for i, x, y in self._moving]
409       
410        self.emit(SIGNAL("editingFinished()"))
411       
412    def deleteSelected(self, *args):
413        data = self.graph.data
414        attr1, attr2 = self.graph.attr1, self.graph.attr2
415        path = self.selection.path()
416        selected = [i for i, ex in enumerate(data) if path.contains(QPointF(float(ex[attr1]) , float(ex[attr2])))]
417        for i in reversed(selected):
418            del data[i]
419        self.graph.updateGraph()
420        if selected:
421            self.emit(SIGNAL("editing()"))
422            self.emit(SIGNAL("editingFinished()"))
423       
424class GraphLassoSelections(GraphSelections):
425    def start(self, event):
426        pos = self.getPos(event)
427        index = self.regionAt(event)
428        if index == -1:
429            self.clearSelection()
430            self.addSelectionRegion([pos])
431        else:
432            self._moving_index, self._moving_pos, self._selection_region = index, pos, self.selection[index]
433            self.emit(SIGNAL("selectionRegionMoveStarted(int, QPointF, QPainterPath)"), index, pos, self.toPath(self.selection[index])) 
434        self.emit(SIGNAL("selectionGeometryChanged()"))
435       
436    def update(self, event):
437        pos = self.getPos(event)
438        index = self._moving_index
439        if index == -1:
440            self.selection[-1].append(pos)
441            self.emit(SIGNAL("selectionRegionUpdated(int, QPainterPath)"), len(self.selection) - 1 , self.toPath(self.selection[-1]))
442        else:
443            diff = self._moving_pos - pos
444            self.selection[index] = [p - diff for p in self._selection_region]
445            self.emit(SIGNAL("selectionRegionMoved(int, QPointF, QPainterPath)"), index, pos, self.toPath(self.selection[index]))
446           
447        self.emit(SIGNAL("selectionGeometryChanged()"))
448       
449    def end(self, event):
450        self.update(event)
451        if self._moving_index != -1:
452            self.emit(SIGNAL("selectionRegionMoveFinished(int, QPointF, QPainterPath)"), 
453                      self._moving_index, self.getPos(event),
454                      self.toPath(self.selection[self._moving_index]))
455        self._moving_index = -1
456       
457       
458class LassoTool(SelectTool):
459    def __init__(self, graph, parent=None):
460        SelectTool.__init__(self, graph, parent, 
461                            graphSelection=GraphLassoSelections(graph))
462#        self.selection = GraphLassoSelections(graph)
463#        self.pen = QPen(Qt.black, 1, Qt.DashDotLine)
464#        self.pen.setCosmetic(True)
465#        self.pen.setJoinStyle(Qt.RoundJoin)
466#        self.pen.setCapStyle(Qt.RoundCap)
467#        self.connect(self.selection, SIGNAL("selectionRegionMoveStarted(int, QPointF, QPainterPath)"), self.onMoveStarted)
468#        self.connect(self.selection, SIGNAL("selectionRegionMoved(int, QPointF, QPainterPath)"), self.onMove)
469#        self.connect(self.selection, SIGNAL("selectionRegionMoveFinished(int, QPointF, QPainterPath)"), self.onMoveFinished)
470   
471   
472class ZoomTool(DataTool):
473    def __init__(self, graph, parent=None):
474        DataTool.__init__(self, graph, parent)
475       
476    def paintEvent(self, event):
477        return False
478   
479    def mousePressEvent(self, event):
480        return False
481   
482    def mouseMoveEvent(self, event):
483        return False
484   
485    def mouseReleaseEvent(self, event):
486        return False
487   
488    def mouseDoubleClickEvent(self, event):
489        return False
490   
491    def keyPressEvent(self, event):
492        return False
493   
494   
495class PutInstanceTool(DataTool):
496    cursor = Qt.CrossCursor
497    def mousePressEvent(self, event):
498        if event.buttons() & Qt.LeftButton:
499            coord = self.invTransform(event.pos())
500            val1, val2 = coord.x(), coord.y()
501            attr1, attr2 = self.attributes()
502            self.dataTransform(attr1, val1, attr2, val2)
503            self.emit(SIGNAL("editing()"))
504            self.emit(SIGNAL("editingFinished()"))
505        return True
506       
507    def dataTransform(self, attr1, val1, attr2, val2):
508        example = orange.Example(self.graph.data.domain)
509        example[attr1] = val1
510        example[attr2] = val2
511        example.setclass(self.graph.data.domain.classVar(self.graph.data.domain.classVar.baseValue))
512        self.graph.data.append(example)
513        self.graph.updateGraph(dataInterval=(-1, sys.maxint))
514       
515       
516class BrushTool(DataTool):
517    brushRadius = 20
518    density = 5
519    cursor = Qt.CrossCursor
520   
521    class optionsWidget(QFrame):
522        def __init__(self, tool, parent=None):
523            QFrame.__init__(self, parent)
524            self.tool = tool
525            layout = QFormLayout()
526            self.radiusSlider = QSlider(Qt.Horizontal)
527            self.radiusSlider.pyqtConfigure(minimum=10, maximum=30, value=self.tool.brushRadius)
528            self.densitySlider = QSlider(Qt.Horizontal)
529            self.densitySlider.pyqtConfigure(minimum=3, maximum=10, value=self.tool.density)
530           
531            layout.addRow("Radius", self.radiusSlider)
532            layout.addRow("Density", self.densitySlider)
533            self.setLayout(layout)
534           
535            self.connect(self.radiusSlider, SIGNAL("valueChanged(int)"),
536                         lambda value: setattr(self.tool, "brushRadius", value))
537           
538            self.connect(self.densitySlider, SIGNAL("valueChanged(int)"),
539                         lambda value: setattr(self.tool, "density", value))
540   
541    def __init__(self, graph, parent=None):
542        DataTool.__init__(self, graph, parent)
543        self.brushState = -20, -20, 0, 0
544   
545    def mousePressEvent(self, event):
546        self.brushState = event.pos().x(), event.pos().y(), self.brushRadius, self.brushRadius
547        x, y, rx, ry = self.brushGeometry(event.pos())
548        if event.buttons() & Qt.LeftButton:
549            attr1, attr2 = self.attributes()
550            self.dataTransform(attr1, x, rx, attr2, y, ry)
551            self.emit(SIGNAL("editing()"))
552        self.graph.replot()
553        return True
554       
555    def mouseMoveEvent(self, event):
556        self.brushState = event.pos().x(), event.pos().y(), self.brushRadius, self.brushRadius
557        x, y, rx, ry = self.brushGeometry(event.pos())
558        if event.buttons() & Qt.LeftButton:
559            attr1, attr2 = self.attributes()
560            self.dataTransform(attr1, x, rx, attr2, y, ry)
561            self.emit(SIGNAL("editing()"))
562        self.graph.replot()
563        return True
564   
565    def mouseReleaseEvent(self, event):
566        self.graph.replot()
567        if event.button() & Qt.LeftButton:
568            self.emit(SIGNAL("editingFinished()"))
569        return True
570   
571    def leaveEvent(self, event):
572        self.graph._tool_pixmap = None
573        self.graph.replot()
574        return False
575       
576    def paintEvent(self, event):
577        if not self.graph.canvas().underMouse():
578            self.graph._tool_pixmap = None
579            return False 
580           
581        pixmap = QPixmap(self.graph.canvas().size())
582        pixmap.fill(QColor(255, 255, 255, 0))
583        painter = QPainter(pixmap)
584        painter.setRenderHint(QPainter.Antialiasing)
585        try:
586            painter.setPen(QPen(Qt.black, 1))
587            x, y, w, h = self.brushState
588            painter.drawEllipse(QPoint(x, y), w, h)
589        except Exception, ex:
590            print ex
591        del painter
592        self.graph._tool_pixmap = pixmap
593        return False
594       
595    def brushGeometry(self, point):
596        coord = self.invTransform(point)
597        dcoord = self.invTransform(QPoint(point.x() + self.brushRadius, point.y() + self.brushRadius))
598        x, y = coord.x(), coord.y()
599        rx, ry = dcoord.x() - x, -(dcoord.y() - y)
600        return x, y, rx, ry
601   
602    def dataTransform(self, attr1, x, rx, attr2, y, ry):
603        import random
604        new = []
605        for i in range(self.density):
606            ex = orange.Example(self.graph.data.domain)
607            ex[attr1] = random.normalvariate(x, rx)
608            ex[attr2] = random.normalvariate(y, ry)
609            ex.setclass(self.graph.data.domain.classVar(self.graph.data.domain.classVar.baseValue))
610            new.append(ex)
611        self.graph.data.extend(new)
612        self.graph.updateGraph(dataInterval=(-len(new), sys.maxint))
613   
614   
615class MagnetTool(BrushTool):
616    cursor = Qt.ArrowCursor
617    def dataTransform(self, attr1, x, rx, attr2, y, ry):
618        for ex in self.graph.data:
619            x1, y1 = float(ex[attr1]), float(ex[attr2])
620            distsq = (x1 - x)**2 + (y1 - y)**2
621            dist = math.sqrt(distsq)
622            attraction = self.density / 100.0
623            advance = 0.005
624            dx = -(x1 - x)/dist * attraction / max(distsq, rx) * advance
625            dy = -(y1 - y)/dist * attraction / max(distsq, ry) * advance
626            ex[attr1] = x1 + dx
627            ex[attr2] = y1 + dy
628        self.graph.updateGraph()
629   
630   
631class JitterTool(BrushTool):
632    cursor = Qt.ArrowCursor
633    def dataTransform(self, attr1, x, rx, attr2, y, ry):
634        import random
635        for ex in self.graph.data:
636            x1, y1 = float(ex[attr1]), float(ex[attr2])
637            distsq = (x1 - x)**2 + (y1 - y)**2
638            dist = math.sqrt(distsq)
639            attraction = self.density / 100.0
640            advance = 0.005
641            dx = -(x1 - x)/dist * attraction / max(distsq, rx) * advance
642            dy = -(y1 - y)/dist * attraction / max(distsq, ry) * advance
643            ex[attr1] = x1 - random.normalvariate(0, dx) #*self.density)
644            ex[attr2] = y1 - random.normalvariate(0, dy) #*self.density)
645        self.graph.updateGraph()
646       
647       
648class EnumVariableModel(PyListModel):
649    def __init__(self, var, parent=None, **kwargs):
650        PyListModel.__init__(self, [], parent, **kwargs)
651        self.wrap(var.values)
652        self.colorPalette = OWColorPalette.ColorPaletteHSV(len(self))
653        self.connect(self, SIGNAL("columnsInserted(QModelIndex, int, int)"), self.updateColors)
654        self.connect(self, SIGNAL("columnsRemoved(QModelIndex, int, int)"), self.updateColors)
655
656    def __delitem__(self, index):
657        raise TypeErorr("Cannot delete EnumVariable value")
658   
659    def __delslice__(self, i, j):
660        raise TypeErorr("Cannot delete EnumVariable values")
661   
662    def __setitem__(self, index, item):
663        self._list[index] = str(item)
664       
665    def data(self, index, role=Qt.DisplayRole):
666        if role == Qt.DecorationRole:
667            i = index.row()
668            return QVariant(self.itemQIcon(i))
669        else:
670            return PyListModel.data(self, index, role)
671       
672    def updateColors(self, index, start, end):
673        self.colorPalette = OWColorPalette.ColorPaletteHSV(len(self))
674        self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(0), self.index(len(self) - 1))
675       
676    def itemQIcon(self, i):
677        pixmap = QPixmap(64, 64)
678        pixmap.fill(QColor(255, 255, 255, 0))
679        painter = QPainter(pixmap)
680        painter.setRenderHint(QPainter.Antialiasing, True)
681        painter.setBrush(self.colorPalette[i])
682        painter.drawEllipse(QRectF(15, 15, 39, 39))
683        painter.end()
684        return QIcon(pixmap)
685   
686   
687class OWPaintData(OWWidget):
688    TOOLS = [("Brush", "Create multiple instances", BrushTool,  icon_brush),
689             ("Put", "Put individual instances", PutInstanceTool, icon_put),
690             ("Select", "Select and move instances", SelectTool, icon_select),
691             ("Lasso", "Select and move instances", LassoTool, icon_lasso),
692             ("Jitter", "Jitter instances", JitterTool, icon_jitter),
693             ("Magnet", "Move (drag) multiple instances", MagnetTool, icon_magnet),
694             ("Zoom", "Zoom", ZoomTool, OWToolbars.dlg_zoom) #"GenerateDataZoomTool.png")
695             ]
696    settingsList = ["commitOnChange"]
697    def __init__(self, parent=None, signalManager=None, name="Data Generator"):
698        OWWidget.__init__(self, parent, signalManager, name)
699       
700        self.outputs = [("Data", ExampleTable)]
701       
702        self.addClassAsMeta = False
703        self.attributes = []
704        self.cov = []
705        self.commitOnChange = False
706       
707        self.loadSettings()
708       
709        self.variablesModel = VariableListModel([orange.FloatVariable(name) for name in ["X", "Y"]], self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable)
710
711       
712        self.classVariable = orange.EnumVariable("Class label", values=["Class 1", "Class 2"], baseValue=0)
713
714        ur = OWGUI.widgetBox(self.controlArea, "Undo / Redo")
715        undo = QAction("Undo", self)
716        undo.pyqtConfigure(toolTip="Undo action")
717        self.connect(undo, SIGNAL("triggered()"), self.undoAction)
718
719        redo = QAction("Redo", self)
720        redo.pyqtConfigure(toolTip="Redo action")
721        self.connect(redo, SIGNAL("triggered()"), self.redoAction)
722
723        actionsWidget =  ModelActionsWidget([undo, redo], self)
724        actionsWidget.layout().addStretch(10)
725        actionsWidget.layout().setSpacing(1)
726
727        ur.layout().addWidget(actionsWidget)
728
729
730        w = OWGUI.widgetBox(self.controlArea, "Class Label")
731       
732        self.classValuesView = listView = QListView()
733        listView.setSelectionMode(QListView.SingleSelection)
734       
735        self.classValuesModel = EnumVariableModel(self.classVariable, self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled| Qt.ItemIsEditable)
736        self.classValuesModel.wrap(self.classVariable.values)
737       
738        listView.setModel(self.classValuesModel)
739        listView.selectionModel().select(self.classValuesModel.index(0), QItemSelectionModel.ClearAndSelect)
740        self.connect(listView.selectionModel(), SIGNAL("selectionChanged(QItemSelection, QItemSelection)"), self.onClassLabelSelection)
741        listView.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum)
742        w.layout().addWidget(listView)
743       
744        self.addClassLabel = addClassLabel = QAction("+", self)
745        addClassLabel.pyqtConfigure(toolTip="Add class label")#, icon=QIcon(icon_put))
746        self.connect(addClassLabel, SIGNAL("triggered()"), self.addNewClassLabel)
747       
748        self.removeClassLabel = removeClassLabel = QAction("-", self)
749        removeClassLabel.pyqtConfigure(toolTip="Remove class label")#, icon=QIcon(icon_remove))
750        self.connect(removeClassLabel, SIGNAL("triggered()"), self.removeSelectedClassLabel)
751       
752        actionsWidget =  ModelActionsWidget([addClassLabel, removeClassLabel], self)
753        actionsWidget.layout().addStretch(10)
754        actionsWidget.layout().setSpacing(1)
755       
756        w.layout().addWidget(actionsWidget)
757       
758        toolbox = OWGUI.widgetBox(self.controlArea, "Tools", orientation=QGridLayout())
759        self.toolActions = QActionGroup(self)
760        self.toolActions.setExclusive(True)
761        for i, (name, tooltip, tool, icon) in enumerate(self.TOOLS):
762            action = QAction(name, self)
763            action.setToolTip(tooltip)
764            action.setCheckable(True)
765            if os.path.exists(icon):
766                action.setIcon(QIcon(icon))
767            self.connect(action, SIGNAL("triggered()"), lambda tool=tool: self.onToolAction(tool))
768            button = QToolButton()
769            button.setDefaultAction(action)
770            button.setIconSize(QSize(24, 24))
771            button.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
772            button.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
773            toolbox.layout().addWidget(button, i / 3, i % 3)
774            self.toolActions.addAction(action)
775           
776        for column in range(3):
777            toolbox.layout().setColumnMinimumWidth(column, 10)
778            toolbox.layout().setColumnStretch(column, 1)
779           
780        self.optionsLayout = QStackedLayout()
781        self.toolsStackCache = {}
782        optionsbox = OWGUI.widgetBox(self.controlArea, "Options", orientation=self.optionsLayout)
783       
784#        OWGUI.checkBox(self.controlArea, self, "addClassAsMeta", "Add class ids as meta attributes")
785        OWGUI.rubber(self.controlArea)
786        box = OWGUI.widgetBox(self.controlArea, "Commit")
787       
788        cb = OWGUI.checkBox(box, self, "commitOnChange", "Commit on change",
789                            tooltip="Send the data on any change.",
790                            callback=self.commitIf,)
791        b = OWGUI.button(box, self, "Commit", 
792                         callback=self.commit, default=True)
793        OWGUI.setStopper(self, b, cb, "dataChangedFlag", callback=self.commit)
794       
795        self.graph = PaintDataGraph(self)
796        self.graph.setAxisScale(QwtPlot.xBottom, 0.0, 1.0)
797        self.graph.setAxisScale(QwtPlot.yLeft, 0.0, 1.0)
798        self.graph.setAttribute(Qt.WA_Hover, True)
799        self.mainArea.layout().addWidget(self.graph)
800
801       
802        self.currentOptionsWidget = None
803        self.data = []
804        self.dataChangedFlag = False 
805        self.domain = None
806       
807        self.onDomainChanged()
808        self.toolActions.actions()[0].trigger()
809
810        self.dataHistory = [(orange.ExampleTable(self.domain), ["Class 1", "Class 2"])]
811        self.historyCounter = 0
812        self.updateHistoryBool = True
813       
814        self.resize(800, 600)
815
816    def addNewClassLabel(self):
817        i = 1
818        while True:
819            newlabel = "Class %i" %i
820            if newlabel not in self.classValuesModel:
821#                self.classValuesModel.append(newlabel)
822                break
823            i += 1
824        values = list(self.classValuesModel) + [newlabel]
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 self.graph.data:
829            newdata.append(orange.Example(newdomain, [ex[a] for a in ex.domain.attributes] + [str(ex.getclass())]))
830       
831        self.classVariable = newclass
832        self.classValuesModel.wrap(self.classVariable.values)
833       
834        self.graph.data = newdata
835        self.graph.updateGraph()
836       
837        newindex = self.classValuesModel.index(len(self.classValuesModel) - 1)
838        self.classValuesView.selectionModel().select(newindex, QItemSelectionModel.ClearAndSelect)
839       
840        self.removeClassLabel.setEnabled(len(self.classValuesModel) > 1)
841
842        self.updateHistory()
843       
844    def removeSelectedClassLabel(self, label=None):
845        index = self.selectedClassLabelIndex()
846        if index is not None and len(self.classValuesModel) > 1:
847            if not label:
848                label = self.classValuesModel[index]
849            examples = [ex for ex in self.graph.data if str(ex.getclass()) != label]
850            print examples
851           
852            values = [val for val in self.classValuesModel if val != label]
853            print "we have values"
854            newclass = orange.EnumVariable("Class label", values=values)
855            newdomain = orange.Domain(self.graph.data.domain.attributes, newclass)
856            newdata = orange.ExampleTable(newdomain)
857            for ex in examples:
858                if ex[self.classVariable] != label and ex[self.classVariable] in values:
859                    newdata.append(orange.Example(newdomain, [ex[a] for a in ex.domain.attributes] + [str(ex.getclass())]))
860
861            self.classVariable = newclass
862            self.classValuesModel.wrap(self.classVariable.values)
863           
864            self.graph.data = newdata
865            self.graph.updateGraph()
866           
867            newindex = self.classValuesModel.index(max(0, index - 1))
868            self.classValuesView.selectionModel().select(newindex, QItemSelectionModel.ClearAndSelect)
869           
870            self.removeClassLabel.setEnabled(len(self.classValuesModel) > 1)
871
872            self.updateHistory()
873       
874    def selectedClassLabelIndex(self):
875        rows = [i.row() for i in self.classValuesView.selectionModel().selectedRows()]
876        if rows:
877            return rows[0]
878        else:
879            return None
880       
881    def onClassLabelSelection(self, selected, unselected):
882        index = self.selectedClassLabelIndex()
883        if index is not None:
884            self.classVariable.baseValue = index
885   
886    def onToolAction(self, tool):
887        self.setCurrentTool(tool)
888       
889    def setCurrentTool(self, tool):
890        if tool not in self.toolsStackCache:
891            newtool = tool(None, self)
892            option = newtool.optionsWidget(newtool, self)
893            self.optionsLayout.addWidget(option)
894#            self.connect(newtool, SIGNAL("dataChanged()"), self.graph.updateGraph)
895#            self.connect(newtool, SIGNAL("dataChanged()"), self.onDataChanged)
896            self.connect(newtool, SIGNAL("editing()"), self.onDataChanged)
897            self.connect(newtool, SIGNAL("editingFinished()"), self.commitIf)
898            self.connect(newtool, SIGNAL("editingFinished()"), self.updateHistory)
899            self.toolsStackCache[tool] = (newtool, option)
900       
901        self.currentTool, self.currentOptionsWidget = tool, option = self.toolsStackCache[tool]
902        self.optionsLayout.setCurrentWidget(option)
903        self.currentTool.setGraph(self.graph)
904
905    def updateHistory(self):
906        if not self.updateHistoryBool:
907            return
908        if not self.historyCounter == len(self.dataHistory)-1:
909            self.dataHistory = self.dataHistory[self.historyCounter:]
910        labels = list(self.classValuesModel)
911        self.dataHistory.append((copy.deepcopy(self.graph.data), labels))
912        if len(self.dataHistory) > 10:
913            self.dataHistory.pop(0)
914        self.historyCounter += 1
915        self.historyCounter %= len(self.dataHistory)
916        print self.historyCounter
917        print self.dataHistory[self.historyCounter]
918
919    def undoAction(self):
920        if self.historyCounter > 0:
921            self.historyCounter -= 1
922            data, labels = self.dataHistory[self.historyCounter]
923            self.graph.setData(data, 0, 1)
924            self.updateHistoryBool = False
925            if len(self.classValuesModel) > len(labels):
926                diff = set(self.classValuesModel) - set(labels)
927                self.removeSelectedClassLabel(label=diff.pop())
928            if len(self.classValuesModel) < len(labels):
929                self.addNewClassLabel()
930            self.updateHistoryBool = True
931            print self.dataHistory[self.historyCounter]
932
933    def redoAction(self):
934        if self.historyCounter < len(self.dataHistory)-1:
935            self.historyCounter += 1
936            data, labels = self.dataHistory[self.historyCounter]
937            self.graph.setData(data, 0, 1)
938            self.updateHistoryBool = False
939            if len(self.classValuesModel) > len(labels):
940                diff = set(self.classValuesModel) - set(labels)
941                self.removeSelectedClassLabel(label=diff.pop())
942            if len(self.classValuesModel) < len(labels):
943                self.addNewClassLabel()
944            self.updateHistoryBool = True
945            print self.dataHistory[self.historyCounter]
946
947    def onDomainChanged(self, *args):
948        if self.variablesModel:
949            self.domain = orange.Domain(list(self.variablesModel), self.classVariable)
950            if self.data:
951                self.data = orange.ExampleTable(self.domain, self.data)
952            else:
953                self.data = orange.ExampleTable(self.domain)
954            self.graph.setData(self.data, 0, 1)
955           
956    def onDataChanged(self):
957        self.dataChangedFlag = True
958
959    def keyPressEvent(self, event):
960        if event.key() == QtCore.Qt.Key_Delete and isinstance(self.currentTool, SelectTool):
961            self.currentTool.deleteSelected()
962   
963    def commitIf(self):
964        if self.commitOnChange and self.dataChangedFlag:
965            self.commit()
966        else:
967            self.dataChangedFlag = True
968           
969    def commit(self):
970        data = self.graph.data
971        values = set([str(ex.getclass()) for ex in data])
972        if len(values) == 1:
973            # Remove the useless class variable.
974            domain = orange.Domain(data.domain.attributes, None)
975            data = orange.ExampleTable(domain, data)
976        self.send("Data", data)
977
978       
979       
980if __name__ == "__main__":
981    app = QApplication(sys.argv)
982    w = OWPaintData()
983    w.show()
984    app.exec_()
985       
Note: See TracBrowser for help on using the repository browser.