source: orange/orange/OrangeWidgets/Visualize/OWScatterPlot.py @ 9546:2b6cc6f397fe

Revision 9546:2b6cc6f397fe, 23.2 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>Scatterplot</name>
3<description>Scatterplot visualization.</description>
4<contact>Gregor Leban (gregor.leban@fri.uni-lj.si)</contact>
5<icon>icons/ScatterPlot.png</icon>
6<priority>1000</priority>
7"""
8# ScatterPlot.py
9#
10# Show data using scatterplot
11#
12from OWWidget import *
13from OWScatterPlotGraph import *
14from OWkNNOptimization import *
15import orngVizRank
16import OWGUI, OWToolbars, OWColorPalette
17from orngScaleData import *
18from OWGraph import OWGraph
19
20
21###########################################################################################
22##### WIDGET : Scatterplot visualization
23###########################################################################################
24class OWScatterPlot(OWWidget):
25    settingsList = ["graph.pointWidth", "graph.showXaxisTitle", "graph.showYLaxisTitle", "showGridlines", "graph.showAxisScale", "graph.useAntialiasing",
26                    "graph.showLegend", "graph.jitterSize", "graph.jitterContinuous", "graph.showFilledSymbols", "graph.showProbabilities",
27                    "graph.alphaValue", "graph.showDistributions", "autoSendSelection", "toolbarSelection", "graph.sendSelectionOnUpdate",
28                    "colorSettings", "selectedSchemaIndex", "VizRankLearnerName"]
29    jitterSizeNums = [0.0, 0.1,   0.5,  1,  2 , 3,  4 , 5 , 7 ,  10,   15,   20 ,  30 ,  40 ,  50 ]
30
31    contextHandlers = {"": DomainContextHandler("", ["attrX", "attrY",
32                                                     (["attrColor", "attrShape", "attrSize"], DomainContextHandler.Optional),
33                                                     ("attrLabel", DomainContextHandler.Optional + DomainContextHandler.IncludeMetaAttributes)])}
34
35    def __init__(self, parent=None, signalManager = None):
36        OWWidget.__init__(self, parent, signalManager, "Scatter Plot", TRUE)
37
38        self.inputs =  [("Data", ExampleTable, self.setData, Default), ("Data Subset", ExampleTable, self.setSubsetData), ("Features", AttributeList, self.setShownAttributes), ("Evaluation Results", orngTest.ExperimentResults, self.setTestResults), ("VizRank Learner", orange.Learner, self.setVizRankLearner)]
39        self.outputs = [("Selected Data", ExampleTable), ("Other Data", ExampleTable)]
40
41        self.graph = OWScatterPlotGraph(self, self.mainArea, "ScatterPlot")
42        self.vizrank = OWVizRank(self, self.signalManager, self.graph, orngVizRank.SCATTERPLOT, "ScatterPlot")
43        self.optimizationDlg = self.vizrank
44
45        # local variables
46        self.showGridlines = 0
47        self.autoSendSelection = 1
48        self.toolbarSelection = 0
49        self.classificationResults = None
50        self.outlierValues = None
51        self.colorSettings = None
52        self.selectedSchemaIndex = 0
53        self.graph.sendSelectionOnUpdate = 0
54        self.attributeSelectionList = None
55
56        self.data = None
57        self.subsetData = None
58
59        #load settings
60        self.loadSettings()
61
62        #GUI
63        self.tabs = OWGUI.tabWidget(self.controlArea)
64        self.GeneralTab = OWGUI.createTabPage(self.tabs, "Main")
65        self.SettingsTab = OWGUI.createTabPage(self.tabs, "Settings", canScroll = True)
66
67        #add a graph widget
68        self.mainArea.layout().addWidget(self.graph)
69        self.connect(self.graphButton, SIGNAL("clicked()"), self.graph.saveToFile)
70
71        #x attribute
72        self.attrX = ""
73        self.attrXCombo = OWGUI.comboBox(self.GeneralTab, self, "attrX", "X-axis Attribute", callback = self.majorUpdateGraph, sendSelectedValue = 1, valueType = str)
74
75        # y attribute
76        self.attrY = ""
77        self.attrYCombo = OWGUI.comboBox(self.GeneralTab, self, "attrY", "Y-axis Attribute", callback = self.majorUpdateGraph, sendSelectedValue = 1, valueType = str)
78
79        # coloring
80        self.attrColor = ""
81        box = OWGUI.widgetBox(self.GeneralTab, "Point Color")
82        self.attrColorCombo = OWGUI.comboBox(box, self, "attrColor", callback = self.updateGraph, sendSelectedValue=1, valueType = str, emptyString = "(Same color)")
83
84        box = OWGUI.widgetBox(self.GeneralTab, "Additional Point Properties")
85        # labelling
86        self.attrLabel = ""
87        self.attrLabelCombo = OWGUI.comboBox(box, self, "attrLabel", label = "Point label:", callback = self.updateGraph, sendSelectedValue = 1, valueType = str, emptyString = "(No labels)", indent = 10)
88
89        # shaping
90        self.attrShape = ""
91        self.attrShapeCombo = OWGUI.comboBox(box, self, "attrShape", label = "Point shape:", callback = self.updateGraph, sendSelectedValue=1, valueType = str, emptyString = "(Same shape)", indent = 10)
92
93        # sizing
94        self.attrSize = ""
95        self.attrSizeCombo = OWGUI.comboBox(box, self, "attrSize", label = "Point size:", callback = self.updateGraph, sendSelectedValue=1, valueType = str, emptyString = "(Same size)", indent = 10)
96
97        self.optimizationButtons = OWGUI.widgetBox(self.GeneralTab, "Optimization dialogs", orientation = "horizontal")
98        OWGUI.button(self.optimizationButtons, self, "VizRank", callback = self.vizrank.reshow, tooltip = "Opens VizRank dialog, where you can search for interesting projections with different subsets of attributes", debuggingEnabled = 0)
99
100        # zooming / selection
101        self.zoomSelectToolbar = OWToolbars.ZoomSelectToolbar(self, self.GeneralTab, self.graph, self.autoSendSelection)
102        self.connect(self.zoomSelectToolbar.buttonSendSelections, SIGNAL("clicked()"), self.sendSelections)
103
104        # ####################################
105        # SETTINGS TAB
106        # point width
107        pointBox = OWGUI.widgetBox(self.SettingsTab, "Point Properties")
108        OWGUI.hSlider(pointBox, self, 'graph.pointWidth', label = "Symbol size:   ", minValue=1, maxValue=20, step=1, callback = self.pointSizeChange)
109        OWGUI.hSlider(pointBox, self, 'graph.alphaValue', label = "Transparency: ", minValue=0, maxValue=255, step=10, callback = self.alphaChange)
110
111        # #####
112        # jittering options
113        box2 = OWGUI.widgetBox(self.SettingsTab, "Jittering Options")
114        self.jitterSizeCombo = OWGUI.comboBox(box2, self, "graph.jitterSize", label = 'Jittering size (% of size)'+'  ', orientation = "horizontal", callback = self.resetGraphData, items = self.jitterSizeNums, sendSelectedValue = 1, valueType = float)
115        OWGUI.checkBox(box2, self, 'graph.jitterContinuous', 'Jitter continuous attributes', callback = self.resetGraphData, tooltip = "Does jittering apply also on continuous attributes?")
116
117        # general graph settings
118        box4 = OWGUI.widgetBox(self.SettingsTab, "General Graph Settings")
119        OWGUI.checkBox(box4, self, 'graph.showXaxisTitle', 'X axis title', callback = self.graph.setShowXaxisTitle)
120        OWGUI.checkBox(box4, self, 'graph.showYLaxisTitle', 'Y axis title', callback = self.graph.setShowYLaxisTitle)
121        OWGUI.checkBox(box4, self, 'graph.showAxisScale', 'Show axis scale', callback = self.updateGraph)
122        OWGUI.checkBox(box4, self, 'graph.showLegend', 'Show legend', callback = self.updateGraph)
123        OWGUI.checkBox(box4, self, 'graph.showFilledSymbols', 'Show filled symbols', callback = self.updateGraph)
124        OWGUI.checkBox(box4, self, 'showGridlines', 'Show gridlines', callback = self.setShowGridlines)
125        OWGUI.checkBox(box4, self, 'graph.useAntialiasing', 'Use antialiasing', callback = self.updateGraph)
126
127        box5 = OWGUI.widgetBox(box4, orientation = "horizontal")
128        OWGUI.checkBox(box5, self, 'graph.showProbabilities', 'Show probabilities'+'  ', callback = self.updateGraph, tooltip = "Show a background image with class probabilities")
129        smallWidget = OWGUI.SmallWidgetLabel(box5, pixmap = 1, box = "Advanced settings", tooltip = "Show advanced settings")
130        #OWGUI.rubber(box5)
131
132        box6 = OWGUI.widgetBox(smallWidget.widget, orientation = "horizontal")
133        box7 = OWGUI.widgetBox(smallWidget.widget, orientation = "horizontal")
134
135        OWGUI.widgetLabel(box6, "Granularity:"+"  ")
136        OWGUI.hSlider(box6, self, 'graph.squareGranularity', minValue=1, maxValue=10, step=1, callback = self.updateGraph)
137
138        OWGUI.checkBox(box7, self, 'graph.spaceBetweenCells', 'Show space between cells', callback = self.updateGraph)
139
140        self.colorButtonsBox = OWGUI.widgetBox(self.SettingsTab, "Colors", orientation = "horizontal")
141        OWGUI.button(self.colorButtonsBox, self, "Set Colors", self.setColors, tooltip = "Set the canvas background color, grid color and color palette for coloring continuous variables", debuggingEnabled = 0)
142
143        box5 = OWGUI.widgetBox(self.SettingsTab, "Tooltips Settings")
144        OWGUI.comboBox(box5, self, "graph.tooltipKind", items = ["Don't Show Tooltips", "Show Visible Attributes", "Show All Attributes"], callback = self.updateGraph)
145
146        box = OWGUI.widgetBox(self.SettingsTab, "Auto Send Selected Data When...")
147        OWGUI.checkBox(box, self, 'autoSendSelection', 'Adding/Removing selection areas', callback = self.selectionChanged, tooltip = "Send selected data whenever a selection area is added or removed")
148        OWGUI.checkBox(box, self, 'graph.sendSelectionOnUpdate', 'Moving/Resizing selection areas', tooltip = "Send selected data when a user moves or resizes an existing selection area")
149        self.graph.autoSendSelectionCallback = self.selectionChanged
150
151        self.GeneralTab.layout().addStretch(100)
152        self.SettingsTab.layout().addStretch(100)
153        self.icons = self.createAttributeIconDict()
154
155        self.debugSettings = ["attrX", "attrY", "attrColor", "attrLabel", "attrShape", "attrSize"]
156        self.wdChildDialogs = [self.vizrank]        # used when running widget debugging
157
158        dlg = self.createColorDialog()
159        self.graph.contPalette = dlg.getContinuousPalette("contPalette")
160        self.graph.discPalette = dlg.getDiscretePalette("discPalette")
161        self.graph.setCanvasBackground(dlg.getColor("Canvas"))
162        self.graph.gridCurve.setPen(QPen(dlg.getColor("Grid")))
163
164        self.graph.enableGridXB(self.showGridlines)
165        self.graph.enableGridYL(self.showGridlines)
166
167        apply([self.zoomSelectToolbar.actionZooming, self.zoomSelectToolbar.actionRectangleSelection, self.zoomSelectToolbar.actionPolygonSelection][self.toolbarSelection], [])
168        #self.SettingsTab.resize(self.SettingsTab.sizeHint())
169
170        self.resize(700, 550)
171
172
173    def settingsFromWidgetCallback(self, handler, context):
174        context.selectionPolygons = []
175        for curve in self.graph.selectionCurveList:
176            xs = [curve.x(i) for i in range(curve.dataSize())]
177            ys = [curve.y(i) for i in range(curve.dataSize())]
178            context.selectionPolygons.append((xs, ys))
179
180    def settingsToWidgetCallback(self, handler, context):
181        selections = getattr(context, "selectionPolygons", [])
182        for (xs, ys) in selections:
183            c = SelectionCurve("")
184            c.setData(xs,ys)
185            c.attach(self.graph)
186            self.graph.selectionCurveList.append(c)
187
188    # ##############################################################################################################################################################
189    # SCATTERPLOT SIGNALS
190    # ##############################################################################################################################################################
191
192    def resetGraphData(self):
193        self.graph.rescaleData()
194        self.majorUpdateGraph()
195
196    # receive new data and update all fields
197    def setData(self, data):
198        if data is not None and (len(data) == 0 or len(data.domain) == 0):
199            data = None
200        if self.data and data and self.data.checksum() == data.checksum():
201            return    # check if the new data set is the same as the old one
202
203        self.closeContext()
204        sameDomain = self.data and data and data.domain.checksum() == self.data.domain.checksum() # preserve attribute choice if the domain is the same
205        self.data = data
206        self.vizrank.clearResults()
207        if not sameDomain:
208            self.initAttrValues()
209        self.graph.insideColors = None
210        self.classificationResults = None
211        self.outlierValues = None
212        self.openContext("", self.data)
213
214    # set an example table with a data subset subset of the data. if called by a visual classifier, the update parameter will be 0
215    def setSubsetData(self, subsetData):
216        self.subsetData = subsetData
217        self.vizrank.clearArguments()
218
219    # this is called by OWBaseWidget after setData and setSubsetData are called. this way the graph is updated only once
220    def handleNewSignals(self):
221        self.graph.setData(self.data, self.subsetData)
222        self.vizrank.resetDialog()
223        if self.attributeSelectionList and 0 not in [self.graph.attributeNameIndex.has_key(attr) for attr in self.attributeSelectionList]:
224            self.attrX = self.attributeSelectionList[0]
225            self.attrY = self.attributeSelectionList[1]
226        self.attributeSelectionList = None
227        self.updateGraph()
228        self.sendSelections()
229
230
231    # receive information about which attributes we want to show on x and y axis
232    def setShownAttributes(self, list):
233        if list and len(list[:2]) == 2:
234            self.attributeSelectionList = list[:2]
235        else:
236            self.attributeSelectionList = None
237
238
239    # visualize the results of the classification
240    def setTestResults(self, results):
241        self.classificationResults = None
242        if isinstance(results, orngTest.ExperimentResults) and len(results.results) > 0 and len(results.results[0].probabilities) > 0:
243            self.classificationResults = [results.results[i].probabilities[0][results.results[i].actualClass] for i in range(len(results.results))]
244            self.classificationResults = (self.classificationResults, "Probability of correct classification = %.2f%%")
245
246
247    # set the learning method to be used in VizRank
248    def setVizRankLearner(self, learner):
249        self.vizrank.externalLearner = learner
250
251    # send signals with selected and unselected examples as two datasets
252    def sendSelections(self):
253        (selected, unselected) = self.graph.getSelectionsAsExampleTables([self.attrX, self.attrY])
254        self.send("Selected Data",selected)
255        self.send("Other Data",unselected)
256
257
258    # ##############################################################################################################################################################
259    # CALLBACKS FROM VIZRANK DIALOG
260    # ##############################################################################################################################################################
261
262    def showSelectedAttributes(self):
263        val = self.vizrank.getSelectedProjection()
264        if not val: return
265        if self.data.domain.classVar:
266            self.attrColor = self.data.domain.classVar.name
267        self.majorUpdateGraph(val[3])
268
269    # ##############################################################################################################################################################
270    # ATTRIBUTE SELECTION
271    # ##############################################################################################################################################################
272
273    def getShownAttributeList(self):
274        return [self.attrX, self.attrY]
275
276    def initAttrValues(self):
277        self.attrXCombo.clear()
278        self.attrYCombo.clear()
279        self.attrColorCombo.clear()
280        self.attrLabelCombo.clear()
281        self.attrShapeCombo.clear()
282        self.attrSizeCombo.clear()
283
284        if not self.data: return
285
286        self.attrColorCombo.addItem("(Same color)")
287        self.attrLabelCombo.addItem("(No labels)")
288        self.attrShapeCombo.addItem("(Same shape)")
289        self.attrSizeCombo.addItem("(Same size)")
290
291        #labels are usually chosen from meta variables, put them on top
292        for metavar in [self.data.domain.getmeta(mykey) for mykey in self.data.domain.getmetas().keys()]:
293            self.attrLabelCombo.addItem(self.icons[metavar.varType], metavar.name)
294
295        contList = []
296        discList = []
297        for attr in self.data.domain:
298            if attr.varType in [orange.VarTypes.Discrete, orange.VarTypes.Continuous]:
299                self.attrXCombo.addItem(self.icons[attr.varType], attr.name)
300                self.attrYCombo.addItem(self.icons[attr.varType], attr.name)
301                self.attrColorCombo.addItem(self.icons[attr.varType], attr.name)
302                self.attrSizeCombo.addItem(self.icons[attr.varType], attr.name)
303            if attr.varType == orange.VarTypes.Discrete: 
304                self.attrShapeCombo.addItem(self.icons[attr.varType], attr.name)
305            self.attrLabelCombo.addItem(self.icons[attr.varType], attr.name)
306
307        self.attrX = str(self.attrXCombo.itemText(0))
308        if self.attrYCombo.count() > 1: self.attrY = str(self.attrYCombo.itemText(1))
309        else:                           self.attrY = str(self.attrYCombo.itemText(0))
310
311        if self.data.domain.classVar and self.data.domain.classVar.varType in [orange.VarTypes.Discrete, orange.VarTypes.Continuous]:
312            self.attrColor = self.data.domain.classVar.name
313        else:
314            self.attrColor = ""
315        self.attrShape = ""
316        self.attrSize= ""
317        self.attrLabel = ""
318
319    def majorUpdateGraph(self, attrList = None, insideColors = None, **args):
320        self.graph.removeAllSelections()
321        self.updateGraph(attrList, insideColors, **args)
322
323    def updateGraph(self, attrList = None, insideColors = None, **args):
324        self.graph.zoomStack = []
325        if not self.graph.haveData:
326            return
327
328        if attrList and len(attrList) == 2:
329            self.attrX = attrList[0]
330            self.attrY = attrList[1]
331
332        if self.graph.dataHasDiscreteClass and (self.vizrank.showKNNCorrectButton.isChecked() or self.vizrank.showKNNWrongButton.isChecked()):
333            kNNExampleAccuracy, probabilities = self.vizrank.kNNClassifyData(self.graph.createProjectionAsExampleTable([self.graph.attributeNameIndex[self.attrX], self.graph.attributeNameIndex[self.attrY]]))
334            if self.vizrank.showKNNCorrectButton.isChecked(): kNNExampleAccuracy = ([1.0 - val for val in kNNExampleAccuracy], "Probability of wrong classification = %.2f%%")
335            else: kNNExampleAccuracy = (kNNExampleAccuracy, "Probability of correct classification = %.2f%%")
336        else:
337            kNNExampleAccuracy = None
338
339        self.graph.insideColors = insideColors or self.classificationResults or kNNExampleAccuracy or self.outlierValues
340        self.graph.updateData(self.attrX, self.attrY, self.attrColor, self.attrShape, self.attrSize, self.attrLabel)
341
342
343    # ##############################################################################################################################################################
344    # SCATTERPLOT SETTINGS
345    # ##############################################################################################################################################################
346    def saveSettings(self):
347        OWWidget.saveSettings(self)
348        self.vizrank.saveSettings()
349
350    #update status on progress bar - gets called by OWScatterplotGraph
351    def updateProgress(self, current, total):
352        self.progressBar.setTotalSteps(total)
353        self.progressBar.setProgress(current)
354
355    def alphaChange(self):
356        for curve in self.graph.itemList():
357            if isinstance(curve, QwtPlotCurve):
358                brushColor = curve.symbol().brush().color()
359                penColor = curve.symbol().pen().color()
360                brushColor.setAlpha(self.graph.alphaValue)
361                brush = QBrush(curve.symbol().brush())
362                brush.setColor(brushColor)
363                penColor.setAlpha(self.graph.alphaValue)
364                symbol = curve.symbol()
365                symbol.setBrush(brush)
366                symbol.setPen(QPen(penColor))
367                if QWT_VERSION_STR >= "5.2" and not curve.testItemAttribute(QwtPlotItem.Legend): # if curve has a legend it is duplicated for all symbols
368                    curve.setSymbol(symbol)
369#                curve.symbol().setBrush(brush)
370#                curve.symbol().setPen(QPen(penColor))
371        self.graph.replot()
372
373    def pointSizeChange(self):
374        if self.attrSize:
375            self.updateGraph()
376        else:
377            for curve in self.graph.itemList():
378                if isinstance(curve, QwtPlotCurve):
379                    symbol = curve.symbol()
380                    symbol.setSize(self.graph.pointWidth)
381                    if QWT_VERSION_STR >= "5.2" and not curve.testItemAttribute(QwtPlotItem.Legend):
382                        curve.setSymbol(symbol)
383                       
384            self.graph.replot()
385
386    def setShowGridlines(self):
387        self.graph.enableGridXB(self.showGridlines)
388        self.graph.enableGridYL(self.showGridlines)
389
390    def selectionChanged(self):
391        self.zoomSelectToolbar.buttonSendSelections.setEnabled(not self.autoSendSelection)
392        if self.autoSendSelection:
393            self.sendSelections()
394
395    def setColors(self):
396        dlg = self.createColorDialog()
397        if dlg.exec_():
398            self.colorSettings = dlg.getColorSchemas()
399            self.selectedSchemaIndex = dlg.selectedSchemaIndex
400            self.graph.contPalette = dlg.getContinuousPalette("contPalette")
401            self.graph.discPalette = dlg.getDiscretePalette("discPalette")
402            self.graph.setCanvasBackground(dlg.getColor("Canvas"))
403            self.graph.setGridColor(dlg.getColor("Grid"))
404            self.updateGraph()
405
406    def createColorDialog(self):
407        c = OWColorPalette.ColorPaletteDlg(self, "Color Palette")
408        c.createDiscretePalette("discPalette", "Discrete Palette")
409        c.createContinuousPalette("contPalette", "Continuous Palette")
410        box = c.createBox("otherColors", "Other Colors")
411        c.createColorButton(box, "Canvas", "Canvas color", Qt.white)
412        box.layout().addSpacing(5)
413        c.createColorButton(box, "Grid", "Grid color", QColor(215,215,215))
414        box.layout().addSpacing(5)
415        c.setColorSchemas(self.colorSettings, self.selectedSchemaIndex)
416        return c
417
418    def closeEvent(self, ce):
419        self.vizrank.close()
420        OWWidget.closeEvent(self, ce)
421
422
423    def sendReport(self):
424        self.startReport("%s [%s - %s]" % (self.windowTitle(), self.attrX, self.attrY))
425        self.reportSettings("Visualized attributes",
426                            [("X", self.attrX),
427                             ("Y", self.attrY),
428                             self.attrColor and ("Color", self.attrColor),
429                             self.attrLabel and ("Label", self.attrLabel),
430                             self.attrShape and ("Shape", self.attrShape),
431                             self.attrSize and ("Size", self.attrSize)])
432        self.reportSettings("Settings",
433                            [("Symbol size", self.graph.pointWidth),
434                             ("Transparency", self.graph.alphaValue),
435                             ("Jittering", self.graph.jitterSize),
436                             ("Jitter continuous attributes", OWGUI.YesNo[self.graph.jitterContinuous])])
437        self.reportSection("Graph")
438        self.reportImage(self.graph.saveToFileDirect, QSize(400, 400))
439
440#test widget appearance
441if __name__=="__main__":
442    a=QApplication(sys.argv)
443    ow=OWScatterPlot()
444    ow.show()
445    data = orange.ExampleTable(r"../../doc/datasets/brown-selected.tab")
446    ow.setData(data)
447    #ow.setData(orange.ExampleTable("..\\..\\doc\\datasets\\wine.tab"))
448    ow.handleNewSignals()
449    a.exec_()
450    #save settings
451    ow.saveSettings()
Note: See TracBrowser for help on using the repository browser.