source: orange/Orange/OrangeWidgets/Data/OWPaintData.py @ 11780:df916a1a0cb1

Revision 11780:df916a1a0cb1, 39.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 5 months ago (diff)

Replaced hypens with unicode minus character in GUI button labels.

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