source: orange/Orange/OrangeWidgets/Data/OWPaintData.py @ 11719:d26fa6fe99b2

Revision 11719:d26fa6fe99b2, 39.0 KB checked in by blaz <blaz.zupan@…>, 7 months ago (diff)

Updated DESCRIPTION fields.

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