source: orange/Orange/OrangeWidgets/Data/OWPaintData.py @ 10727:1e900efe56ac

Revision 10727:1e900efe56ac, 35.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 2 years ago (diff)

Changed edit triggers.

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