source: orange/orange/OrangeWidgets/Data/OWPaintData.py @ 9546:2b6cc6f397fe

Revision 9546:2b6cc6f397fe, 35.5 KB checked in by ales_erjavec <ales.erjavec@…>, 2 years ago (diff)

Renamed widget channel names in line with the new naming rules/convention.
Added backwards compatibility in orngDoc loadDocument to enable loading of schemas saved before the change.

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        listView.setEditTriggers(QListView.SelectedClicked)
714       
715        self.classValuesModel = EnumVariableModel(self.classVariable, self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled| Qt.ItemIsEditable)
716        self.classValuesModel.wrap(self.classVariable.values)
717       
718        listView.setModel(self.classValuesModel)
719        listView.selectionModel().select(self.classValuesModel.index(0), QItemSelectionModel.ClearAndSelect)
720        self.connect(listView.selectionModel(), SIGNAL("selectionChanged(QItemSelection, QItemSelection)"), self.onClassLabelSelection)
721        listView.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum)
722        w.layout().addWidget(listView)
723       
724        self.addClassLabel = addClassLabel = QAction("+", self)
725        addClassLabel.pyqtConfigure(toolTip="Add class label")#, icon=QIcon(icon_put))
726        self.connect(addClassLabel, SIGNAL("triggered()"), self.addNewClassLabel)
727       
728        self.removeClassLabel = removeClassLabel = QAction("-", self)
729        removeClassLabel.pyqtConfigure(toolTip="Remove class label")#, icon=QIcon(icon_remove))
730        self.connect(removeClassLabel, SIGNAL("triggered()"), self.removeSelectedClassLabel)
731       
732        actionsWidget =  ModelActionsWidget([addClassLabel, removeClassLabel], self)
733        actionsWidget.layout().addStretch(10)
734        actionsWidget.layout().setSpacing(1)
735       
736        w.layout().addWidget(actionsWidget)
737       
738        toolbox = OWGUI.widgetBox(self.controlArea, "Tools", orientation=QGridLayout())
739        self.toolActions = QActionGroup(self)
740        self.toolActions.setExclusive(True)
741        for i, (name, tooltip, tool, icon) in enumerate(self.TOOLS):
742            action = QAction(name, self)
743            action.setToolTip(tooltip)
744            action.setCheckable(True)
745            if os.path.exists(icon):
746                action.setIcon(QIcon(icon))
747            self.connect(action, SIGNAL("triggered()"), lambda tool=tool: self.onToolAction(tool))
748            button = QToolButton()
749            button.setDefaultAction(action)
750            button.setIconSize(QSize(24, 24))
751            button.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
752            button.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
753            toolbox.layout().addWidget(button, i / 3, i % 3)
754            self.toolActions.addAction(action)
755           
756        for column in range(3):
757            toolbox.layout().setColumnMinimumWidth(column, 10)
758            toolbox.layout().setColumnStretch(column, 1)
759           
760        self.optionsLayout = QStackedLayout()
761        self.toolsStackCache = {}
762        optionsbox = OWGUI.widgetBox(self.controlArea, "Options", orientation=self.optionsLayout)
763       
764#        OWGUI.checkBox(self.controlArea, self, "addClassAsMeta", "Add class ids as meta attributes")
765        OWGUI.rubber(self.controlArea)
766        box = OWGUI.widgetBox(self.controlArea, "Commit")
767       
768        cb = OWGUI.checkBox(box, self, "commitOnChange", "Commit on change",
769                            tooltip="Send the data on any change.",
770                            callback=self.commitIf,)
771        b = OWGUI.button(box, self, "Commit", 
772                         callback=self.commit, default=True)
773        OWGUI.setStopper(self, b, cb, "dataChangedFlag", callback=self.commit)
774       
775        self.graph = PaintDataGraph(self)
776        self.graph.setAxisScale(QwtPlot.xBottom, 0.0, 1.0)
777        self.graph.setAxisScale(QwtPlot.yLeft, 0.0, 1.0)
778        self.graph.setAttribute(Qt.WA_Hover, True)
779        self.mainArea.layout().addWidget(self.graph)
780       
781        self.currentOptionsWidget = None
782        self.data = []
783        self.dataChangedFlag = False 
784        self.domain = None
785       
786        self.onDomainChanged()
787        self.toolActions.actions()[0].trigger()
788       
789        self.resize(800, 600)
790       
791    def addNewClassLabel(self):
792        i = 1
793        while True:
794            newlabel = "Class %i" %i
795            if newlabel not in self.classValuesModel:
796#                self.classValuesModel.append(newlabel)
797                break
798            i += 1
799        values = list(self.classValuesModel) + [newlabel]
800        newclass = orange.EnumVariable("Class label", values=values)
801        newdomain = orange.Domain(self.graph.data.domain.attributes, newclass)
802        newdata = orange.ExampleTable(newdomain)
803        for ex in self.graph.data:
804            newdata.append(orange.Example(newdomain, [ex[a] for a in ex.domain.attributes] + [str(ex.getclass())]))
805       
806        self.classVariable = newclass
807        self.classValuesModel.wrap(self.classVariable.values)
808       
809        self.graph.data = newdata
810        self.graph.updateGraph()
811       
812        newindex = self.classValuesModel.index(len(self.classValuesModel) - 1)
813        self.classValuesView.selectionModel().select(newindex, QItemSelectionModel.ClearAndSelect)
814       
815        self.removeClassLabel.setEnabled(len(self.classValuesModel) > 1)
816       
817    def removeSelectedClassLabel(self):
818        index = self.selectedClassLabelIndex()
819        if index is not None and len(self.classValuesModel) > 1:
820            label = self.classValuesModel[index]
821            examples = [ex for ex in self.graph.data if str(ex.getclass()) != label]
822           
823            values = [val for val in self.classValuesModel if val != label]
824            newclass = orange.EnumVariable("Class label", values=values)
825            newdomain = orange.Domain(self.graph.data.domain.attributes, newclass)
826            newdata = orange.ExampleTable(newdomain)
827            for ex in examples:
828                if ex[self.classVariable] != label and ex[self.classVariable] in values:
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(max(0, index - 1))
838            self.classValuesView.selectionModel().select(newindex, QItemSelectionModel.ClearAndSelect)
839           
840            self.removeClassLabel.setEnabled(len(self.classValuesModel) > 1) 
841       
842    def selectedClassLabelIndex(self):
843        rows = [i.row() for i in self.classValuesView.selectionModel().selectedRows()]
844        if rows:
845            return rows[0]
846        else:
847            return None
848       
849    def onClassLabelSelection(self, selected, unselected):
850        index = self.selectedClassLabelIndex()
851        if index is not None:
852            self.classVariable.baseValue = index
853   
854    def onToolAction(self, tool):
855        self.setCurrentTool(tool)
856       
857    def setCurrentTool(self, tool):
858        if tool not in self.toolsStackCache:
859            newtool = tool(None, self)
860            option = newtool.optionsWidget(newtool, self)
861            self.optionsLayout.addWidget(option)
862#            self.connect(newtool, SIGNAL("dataChanged()"), self.graph.updateGraph)
863#            self.connect(newtool, SIGNAL("dataChanged()"), self.onDataChanged)
864            self.connect(newtool, SIGNAL("editing()"), self.onDataChanged)
865            self.connect(newtool, SIGNAL("editingFinished()"), self.commitIf)
866            self.toolsStackCache[tool] = (newtool, option)
867       
868        self.currentTool, self.currentOptionsWidget = tool, option = self.toolsStackCache[tool]
869        self.optionsLayout.setCurrentWidget(option)
870        self.currentTool.setGraph(self.graph)
871       
872    def onDomainChanged(self, *args):
873        if self.variablesModel:
874            self.domain = orange.Domain(list(self.variablesModel), self.classVariable)
875            if self.data:
876                self.data = orange.ExampleTable(self.domain, self.data)
877            else:
878                self.data = orange.ExampleTable(self.domain)
879            self.graph.setData(self.data, 0, 1)
880           
881    def onDataChanged(self):
882        self.dataChangedFlag = True
883   
884    def commitIf(self):
885        if self.commitOnChange and self.dataChangedFlag:
886            self.commit()
887        else:
888            self.dataChangedFlag = True
889           
890    def commit(self):
891        data = self.graph.data
892        values = set([str(ex.getclass()) for ex in data])
893        if len(values) == 1:
894            # Remove the useless class variable.
895            domain = orange.Domain(data.domain.attributes, None)
896            data = orange.ExampleTable(domain, data)
897        self.send("Data", data)
898       
899       
900if __name__ == "__main__":
901    app = QApplication(sys.argv)
902    w = OWPaintData()
903    w.show()
904    app.exec_()
905       
Note: See TracBrowser for help on using the repository browser.