Changeset 11379:6bf8920120ac in orange for Orange/OrangeWidgets/Unsupervised/OWMDS.py
 Timestamp:
 03/07/13 18:22:17 (14 months ago)
 Branch:
 default
 File:

 1 edited
Legend:
 Unmodified
 Added
 Removed

Orange/OrangeWidgets/Unsupervised/OWMDS.py
r11378 r11379 6 6 <priority>2500</priority> 7 7 """ 8 9 import sys 10 import math 11 12 import numpy 13 8 14 from OWWidget import * 9 15 import orange 10 16 import orngMDS 11 17 import OWGUI 12 import numpy, sys, math, time, os 18 13 19 import OWColorPalette 14 20 import OWToolbars 15 21 from OWGraph import * 16 22 from PyQt4.Qwt5 import * 23 17 24 from random import random 18 25 19 26 20 27 class OWMDS(OWWidget): 21 settingsList=["graph.PointSize", "graph.proportionGraphed", "graph.ColorAttr", "graph.SizeAttr", 22 "graph.ShapeAttr", "graph.NameAttr", "graph.ShowStress", "graph.NumStressLines", 23 "graph.ShowName", "graph.differentWidths", "graph.stressByTransparency", "graph.useAntialiasing" 24 "StressFunc", "applyLSMT", "toolbarSelection", "autoSendSelection", "selectionOptions", "computeStress", 25 "RefreshMode"] 26 contextHandlers={"":DomainContextHandler("", [ContextField("graph.ColorAttr", DomainContextHandler.Optional), 27 ContextField("graph.SizeAttr", DomainContextHandler.Optional), 28 ContextField("graph.ShapeAttr", DomainContextHandler.Optional), 29 ContextField("graph.NameAttr", DomainContextHandler.Optional), 30 ContextField("graph.ShowName", DomainContextHandler.Optional)])} 31 32 def __init__(self, parent=None, signalManager=None, name="Multi Dimensional Scaling"): 28 settingsList = [ 29 "graph.PointSize", "graph.proportionGraphed", "graph.ColorAttr", 30 "graph.SizeAttr", "graph.ShapeAttr", "graph.NameAttr", 31 "graph.ShowStress", "graph.NumStressLines", "graph.ShowName", 32 "graph.differentWidths", "graph.stressByTransparency", 33 "graph.useAntialiasing" "StressFunc", "applyLSMT", 34 "toolbarSelection", "autoSendSelection", "selectionOptions", 35 "computeStress", "RefreshMode" 36 ] 37 38 contextHandlers = { 39 "": DomainContextHandler( 40 "", 41 [ContextField("graph.ColorAttr", DomainContextHandler.Optional), 42 ContextField("graph.SizeAttr", DomainContextHandler.Optional), 43 ContextField("graph.ShapeAttr", DomainContextHandler.Optional), 44 ContextField("graph.NameAttr", DomainContextHandler.Optional), 45 ContextField("graph.ShowName", DomainContextHandler.Optional)] 46 ) 47 } 48 49 def __init__(self, parent=None, signalManager=None, 50 name="Multi Dimensional Scaling"): 33 51 OWWidget.__init__(self, parent, signalManager, name, wantGraph=True) 34 self.inputs=[("Distances", orange.SymMatrix, self.cmatrix), ("Data Subset", ExampleTable, self.cselected)] 35 self.outputs=[("Data", ExampleTable)] 36 37 self.StressFunc=3 38 self.minStressDelta=5e5 39 self.maxIterations=5000 40 self.maxImprovment=10 41 self.autoSendSelection=0 42 self.toolbarSelection=0 43 self.selectionOptions=0 44 self.computeStress=1 45 self.ReDraw=1 46 self.NumIter=1 47 self.RefreshMode=0 52 53 self.inputs = [("Distances", orange.SymMatrix, self.cmatrix), 54 ("Data Subset", ExampleTable, self.cselected)] 55 self.outputs = [("Data", ExampleTable)] 56 57 self.StressFunc = 3 58 self.minStressDelta = 5e5 59 self.maxIterations = 5000 60 self.maxImprovment = 10 61 self.autoSendSelection = 0 62 self.toolbarSelection = 0 63 self.selectionOptions = 0 64 self.computeStress = 1 65 self.ReDraw = 1 66 self.NumIter = 1 67 self.RefreshMode = 0 48 68 self.applyLSMT = 0 49 69 50 self.stressFunc =[("Kruskal stress", orngMDS.KruskalStress),51 52 53 54 55 self.graph =MDSGraph(self.mainArea)70 self.stressFunc = [("Kruskal stress", orngMDS.KruskalStress), 71 ("Sammon stress", orngMDS.SammonStress), 72 ("Signed Sammon stress", orngMDS.SgnSammonStress), 73 ("Signed relative stress", orngMDS.SgnRelStress)] 74 75 self.graph = MDSGraph(self.mainArea) 56 76 self.mainArea.layout().addWidget(self.graph) 57 77 58 78 self.loadSettings() 59 79 60 tabs =OWGUI.tabWidget(self.controlArea)61 62 mds =OWGUI.createTabPage(tabs, "MDS")63 graph =OWGUI.createTabPage(tabs, "Graph")64 65 # #MDS Tab66 init =OWGUI.widgetBox(mds, "Initialization")80 tabs = OWGUI.tabWidget(self.controlArea) 81 82 mds = OWGUI.createTabPage(tabs, "MDS") 83 graph = OWGUI.createTabPage(tabs, "Graph") 84 85 # MDS Tab 86 init = OWGUI.widgetBox(mds, "Initialization") 67 87 OWGUI.button(init, self, "Randomize", self.randomize) 68 88 OWGUI.button(init, self, "Jitter", self.jitter) 69 89 OWGUI.button(init, self, "Torgerson", self.torgerson) 70 opt=OWGUI.widgetBox(mds, "Optimization") 71 72 self.startButton=OWGUI.button(opt, self, "Optimize", self.testStart) 90 91 opt = OWGUI.widgetBox(mds, "Optimization") 92 93 self.startButton = OWGUI.button(opt, self, "Optimize", self.testStart) 73 94 OWGUI.button(opt, self, "Single Step", self.smacofStep) 74 95 box = OWGUI.widgetBox(opt, "Stress Function") 75 OWGUI.comboBox(box, self, "StressFunc", items=[a[0] for a in self.stressFunc], callback=self.updateStress) 76 OWGUI.radioButtonsInBox(opt, self, "RefreshMode", ["Every step", "Every 10 steps", "Every 100 steps"], "Refresh During Optimization", callback=lambda :1) 77 78 self.stopping=OWGUI.widgetBox(opt, "Stopping Conditions") 79 80 OWGUI.hSlider(OWGUI.widgetBox(self.stopping, "Min. stress change", flat=True), 81 self, "minStressDelta", minValue=5e5, maxValue=1e2, step=5e5, 82 labelFormat="%.5f", intOnly=0) 83 OWGUI.hSlider(OWGUI.widgetBox(self.stopping, "Max. number of steps", flat=True), 84 self, "maxIterations", minValue=10, maxValue=5000, step=10, 85 labelFormat="%i") 86 87 ##Graph Tab 88 OWGUI.hSlider(graph, self, "graph.PointSize", box="Point Size", minValue=1, maxValue=20, callback=self.graph.updateData) 89 self.colorCombo=OWGUI.comboBox(graph, self, "graph.ColorAttr", box="Color", callback=self.graph.updateData) 90 self.sizeCombo=OWGUI.comboBox(graph, self, "graph.SizeAttr", box="Size", callback=self.graph.updateData) 91 self.shapeCombo=OWGUI.comboBox(graph, self, "graph.ShapeAttr", box="Shape", callback=self.graph.updateData) 92 self.nameCombo=OWGUI.comboBox(graph, self, "graph.NameAttr", box="Label", callback=self.graph.updateData) 93 96 OWGUI.comboBox(box, self, "StressFunc", 97 items=[a[0] for a in self.stressFunc], 98 callback=self.updateStress) 99 100 OWGUI.radioButtonsInBox(opt, self, "RefreshMode", 101 ["Every step", "Every 10 steps", 102 "Every 100 steps"], 103 "Refresh During Optimization") 104 105 self.stopping = OWGUI.widgetBox(opt, "Stopping Conditions") 106 107 OWGUI.hSlider(OWGUI.widgetBox(self.stopping, "Min. stress change", 108 flat=True), 109 self, "minStressDelta", minValue=5e5, maxValue=1e2, 110 step=5e5, labelFormat="%.5f", intOnly=0) 111 112 OWGUI.hSlider(OWGUI.widgetBox(self.stopping, "Max. number of steps", 113 flat=True), 114 self, "maxIterations", minValue=10, maxValue=5000, 115 step=10, labelFormat="%i") 116 117 # Graph Tab 118 OWGUI.hSlider(graph, self, "graph.PointSize", box="Point Size", 119 minValue=1, maxValue=20, callback=self.graph.updateData) 120 121 self.colorCombo = OWGUI.comboBox(graph, self, "graph.ColorAttr", 122 box="Color", 123 callback=self.graph.updateData) 124 self.sizeCombo = OWGUI.comboBox(graph, self, "graph.SizeAttr", 125 box="Size", 126 callback=self.graph.updateData) 127 self.shapeCombo = OWGUI.comboBox(graph, self, "graph.ShapeAttr", 128 box="Shape", 129 callback=self.graph.updateData) 130 self.nameCombo = OWGUI.comboBox(graph, self, "graph.NameAttr", 131 box="Label", 132 callback=self.graph.updateData) 133 94 134 box = OWGUI.widgetBox(graph, "Distances & Stress") 95 OWGUI.checkBox(box, self, "graph.ShowStress", "Show similar pairs", callback = self.graph.updateLinesRepaint) 135 136 OWGUI.checkBox(box, self, "graph.ShowStress", "Show similar pairs", 137 callback=self.graph.updateLinesRepaint) 96 138 b2 = OWGUI.widgetBox(box) 97 139 OWGUI.widgetLabel(b2, "Proportion of connected pairs") 98 140 OWGUI.separator(b2, height=3) 99 sl = OWGUI.hSlider(b2, self, "graph.proportionGraphed", minValue=0, maxValue=20, callback=self.graph.updateLinesRepaint, tooltip="Proportion of connected pairs (Maximum of 1000 lines will be drawn") 100 OWGUI.checkBox(box, self, "graph.differentWidths", "Show distance by line width", callback = self.graph.updateLinesRepaint) 101 OWGUI.checkBox(box, self, "graph.stressByTransparency", "Show stress by transparency", callback = self.graph.updateData) 102 OWGUI.checkBox(box, self, "graph.stressBySize", "Show stress by symbol size", callback = self.updateStressBySize) 141 OWGUI.hSlider(b2, self, "graph.proportionGraphed", minValue=0, 142 maxValue=20, 143 callback=self.graph.updateLinesRepaint, 144 tooltip=("Proportion of connected pairs (Maximum of " 145 "1000 lines will be drawn")) 146 OWGUI.checkBox(box, self, "graph.differentWidths", 147 "Show distance by line width", 148 callback=self.graph.updateLinesRepaint) 149 OWGUI.checkBox(box, self, "graph.stressByTransparency", 150 "Show stress by transparency", 151 callback=self.graph.updateData) 152 OWGUI.checkBox(box, self, "graph.stressBySize", 153 "Show stress by symbol size", 154 callback=self.updateStressBySize) 103 155 self.updateStressBySize(True) 104 105 OWGUI.checkBox(graph, self, "graph.useAntialiasing", label="Use antialiasing", box="Antialiasing", tooltip="Use antialiasing for beter quality graphics", callback=self.graph.updateData) 106 107 self.zoomToolbar=OWToolbars.ZoomSelectToolbar(self, graph, self.graph, self.autoSendSelection) 108 self.connect(self.zoomToolbar.buttonSendSelections, SIGNAL("clicked()"), self.sendSelections) 109 self.graph.autoSendSelectionCallback = lambda :self.autoSendSelection and self.sendSelections() 156 157 OWGUI.checkBox(graph, self, "graph.useAntialiasing", 158 label="Use antialiasing", 159 box="Antialiasing", 160 tooltip="Use antialiasing for beter quality graphics", 161 callback=self.graph.updateData) 162 163 self.zoomToolbar = OWToolbars.ZoomSelectToolbar( 164 self, graph, self.graph, self.autoSendSelection 165 ) 166 167 self.connect(self.zoomToolbar.buttonSendSelections, 168 SIGNAL("clicked()"), 169 self.sendSelections) 170 self.graph.autoSendSelectionCallback = \ 171 lambda: self.autoSendSelection and self.sendSelections() 110 172 111 173 OWGUI.checkBox(graph, self, "autoSendSelection", "Auto send selected") 112 OWGUI.radioButtonsInBox(graph, self, "selectionOptions", ["Don't append", "Append coordinates", "Append coordinates as meta"], box="Append coordinates", callback=self.sendIf) 113 114 mds.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)) 115 graph.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)) 174 OWGUI.radioButtonsInBox(graph, self, "selectionOptions", 175 ["Don't append", "Append coordinates", 176 "Append coordinates as meta"], 177 box="Append coordinates", 178 callback=self.sendIf) 179 180 mds.setSizePolicy( 181 QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) 182 ) 183 graph.setSizePolicy( 184 QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) 185 ) 186 116 187 self.controlArea.setMinimumWidth(250) 188 117 189 OWGUI.rubber(mds) 118 infoBox=OWGUI.widgetBox(mds, "Info") 119 self.infoA=OWGUI.widgetLabel(infoBox, "Avg. stress:") 120 self.infoB=OWGUI.widgetLabel(infoBox, "Num. steps") 121 # OWGUI.button(self.controlArea, self, "Save", self.graph.saveToFile, debuggingEnabled = 0) 122 self.connect(self.graphButton, SIGNAL("clicked()"), self.graph.saveToFile) 123 self.resize(900,630) 124 125 self.done=True 126 self.data=None 127 self.selectedInputExamples=[] 128 self.selectedInput=[] 190 191 infoBox = OWGUI.widgetBox(mds, "Info") 192 self.infoA = OWGUI.widgetLabel(infoBox, "Avg. stress:") 193 self.infoB = OWGUI.widgetLabel(infoBox, "Num. steps") 194 195 self.connect(self.graphButton, 196 SIGNAL("clicked()"), 197 self.graph.saveToFile) 198 199 self.resize(900, 630) 200 201 self.done = True 202 self.data = None 203 self.selectedInputExamples = [] 204 self.selectedInput = [] 129 205 130 206 def cmatrix(self, matrix=None): 131 207 self.closeContext() 132 self.origMatrix =matrix133 self.data =data=None208 self.origMatrix = matrix 209 self.data = data = None 134 210 if matrix: 135 self.data =data=getattr(matrix, "items")136 matrix.matrixType = orange.SymMatrix.Symmetric 137 138 self.graph.ColorAttr =0139 self.graph.SizeAttr =0140 self.graph.ShapeAttr =0141 self.graph.NameAttr =0211 self.data = data = getattr(matrix, "items") 212 matrix.matrixType = orange.SymMatrix.Symmetric 213 214 self.graph.ColorAttr = 0 215 self.graph.SizeAttr = 0 216 self.graph.ShapeAttr = 0 217 self.graph.NameAttr = 0 142 218 self.graph.closestPairs = None 219 143 220 if isinstance(data, orange.ExampleTable): 144 221 self.setExampleTable(data) 145 222 elif isinstance(data, orange.VarList): 146 223 self.setVarList(data) 224 147 225 if matrix: 148 self.mds=orngMDS.MDS(matrix) 149 self.mds.points=numpy.random.random(size=[self.mds.n, self.mds.dim]) 226 self.mds = orngMDS.MDS(matrix) 227 self.mds.points = numpy.random.random( 228 size=[self.mds.n, self.mds.dim] 229 ) 230 150 231 self.mds.getStress() 151 self.stress =self.getAvgStress(self.stressFunc[self.StressFunc][1])232 self.stress = self.getAvgStress(self.stressFunc[self.StressFunc][1]) 152 233 if data and type(data) == orange.ExampleTable: 153 self.openContext("",self.data) 154 self.graph.setData(self.mds, self.colors, self.sizes, self.shapes, self.names, self.selectedInput) 234 self.openContext("", self.data) 235 self.graph.setData(self.mds, self.colors, self.sizes, self.shapes, 236 self.names, self.selectedInput) 155 237 else: 156 238 self.graph.clear() 157 239 158 240 def cselected(self, selected=[]): 159 self.selectedInputExamples =selected and selected or[]160 if self.data and type(self.data) ==orange.ExampleTable:241 self.selectedInputExamples = selected and selected or[] 242 if self.data and type(self.data) == orange.ExampleTable: 161 243 self.setExampleTable(self.data) 162 self.graph.setData(self.mds, self.colors, self.sizes, self.shapes, self.names, self.selectedInput) 244 self.graph.setData(self.mds, self.colors, self.sizes, self.shapes, 245 self.names, self.selectedInput) 163 246 164 247 def setExampleTable(self, data): … … 167 250 self.shapeCombo.clear() 168 251 self.nameCombo.clear() 169 attributes=[attr for attr in data.domain.variables+data.domain.getmetas().values() or [] ] 170 discAttributes=filter(lambda a: a.varType==orange.VarTypes.Discrete, attributes) 171 contAttributes=filter(lambda a: a.varType==orange.VarTypes.Continuous, attributes) 172 attrName=[attr.name for attr in attributes] 173 for name in ["Same color"]+attrName: 252 attributes = list(data.domain.variables) + \ 253 data.domain.getmetas().values() 254 discAttributes = filter( 255 lambda a: a.varType == orange.VarTypes.Discrete, 256 attributes 257 ) 258 contAttributes = filter( 259 lambda a: a.varType == orange.VarTypes.Continuous, 260 attributes 261 ) 262 attrName = [attr.name for attr in attributes] 263 for name in ["Same color"] + attrName: 174 264 self.colorCombo.addItem(name) 175 for name in ["Same size"] +map(lambda a:a.name, contAttributes):265 for name in ["Same size"] + map(lambda a: a.name, contAttributes): 176 266 self.sizeCombo.addItem(name) 177 for name in ["Same shape"] +map(lambda a: a.name, discAttributes):267 for name in ["Same shape"] + map(lambda a: a.name, discAttributes): 178 268 self.shapeCombo.addItem(name) 179 for name in ["No name"] +attrName:269 for name in ["No name"] + attrName: 180 270 self.nameCombo.addItem(name) 181 271 182 # if data.domain.classVar:183 # if data.domain.classVar.varType == orange.VarTypes.Discrete:184 # self.graph.ColorAttr = len(data.domain.variables) # index 0 is Same color!185 # elif data.domain.classVar.varType == orange.VarTypes.Continuous:186 # self.graph.SizeAttr = len(data.domain.variables) # index 0 is Same color!187 272 try: 188 self.graph.NameAttr = 1 + [name.lower() for name in attrName].index("name") 273 self.graph.NameAttr = \ 274 1 + [name.lower() for name in attrName].index("name") 189 275 except: 190 276 pass 191 192 self.attributes=attributes 193 self.discAttributes=discAttributes 194 self.contAttributes=contAttributes 195 196 self.colors=[[Qt.black]*(len(attributes)+1) for i in range(len(data))] 197 self.shapes=[[QwtSymbol.Ellipse]*(len(discAttributes)+1) for i in range(len(data))] 198 self.sizes=[[5]*(len(contAttributes)+1) for i in range(len(data))] 199 self.names=[[""]*(len(attributes)+1) for i in range(len(data))] 277 278 self.attributes = attributes 279 self.discAttributes = discAttributes 280 self.contAttributes = contAttributes 281 282 self.colors = [[Qt.black] * (len(attributes) + 1) 283 for i in range(len(data))] 284 self.shapes = [[QwtSymbol.Ellipse] * (len(discAttributes) + 1) 285 for i in range(len(data))] 286 self.sizes = [[5] * (len(contAttributes) + 1) 287 for i in range(len(data))] 288 self.names = [[""] * (len(attributes) + 1) 289 for i in range(len(data))] 200 290 try: 201 selectedInput =self.selectedInputExamples.select(data.domain)291 selectedInput = self.selectedInputExamples.select(data.domain) 202 292 except: 203 selectedInput=[] 204 self.selectedInput=map(lambda d: selectedInput and (d in selectedInput) or not selectedInput, data) 205 contI=discI=attrI=1 293 selectedInput = [] 294 self.selectedInput = map( 295 lambda d: selectedInput and (d in selectedInput) or not selectedInput, 296 data 297 ) 298 299 contI = discI = attrI = 1 300 206 301 def check(ex, a): 207 302 try: 208 303 ex[a] 209 except :304 except Exception: 210 305 return False 211 306 return not ex[a].isSpecial() 212 307 213 308 for j, attr in enumerate(attributes): 214 if attr.varType ==orange.VarTypes.Discrete:215 c =OWColorPalette.ColorPaletteHSV(len(attr.values))309 if attr.varType == orange.VarTypes.Discrete: 310 c = OWColorPalette.ColorPaletteHSV(len(attr.values)) 216 311 for i in range(len(data)): 217 self.colors[i][attrI]= check(data[i],attr) and c[int(data[i][attr])] or Qt.black 218 ## self.shapes[i][discI]= data[i][attr].isSpecial() and self.graph.shapeList[0] or self.graph.shapeList[int(data[i][attr])%len(self.graph.shapeList)] 219 self.shapes[i][discI]= check(data[i],attr) and self.graph.shapeList[int(data[i][attr])%len(self.graph.shapeList)] or self.graph.shapeList[0] 220 self.names[i][attrI]= check(data[i],attr) and " "+str(data[i][attr]) or "" 221 #self.sizes[i][contI]=5 222 attrI+=1 223 discI+=1 224 elif attr.varType==orange.VarTypes.Continuous: 225 c=OWColorPalette.ColorPaletteBW(1) 226 #val=[e[attr] for e in data if not e[attr].isSpecial()] 227 val=[e[attr] for e in data if check(e, attr)] 228 minVal=min(val or [0]) 229 maxVal=max(val or [1]) 312 if check(data[i], attr): 313 self.colors[i][attrI] = c[int(data[i][attr])] 314 self.shapes[i][discI] = self.graph.shapeList[int(data[i][attr]) % len(self.graph.shapeList)] 315 self.names[i][attrI] = " " + str(data[i][attr]) 316 else: 317 self.colors[i][attrI] = Qt.black 318 self.shapes[i][discI] = self.graph.shapeList[0] 319 self.names[i][attrI] = "" 320 attrI += 1 321 discI += 1 322 elif attr.varType == orange.VarTypes.Continuous: 323 c = OWColorPalette.ColorPaletteBW(1) 324 325 val = [e[attr] for e in data if check(e, attr)] 326 minVal = min(val or [0]) 327 maxVal = max(val or [1]) 328 span = max(maxVal  minVal, 1e6) 230 329 for i in range(len(data)): 231 self.colors[i][attrI]=check(data[i],attr) and c.getColor((data[i][attr]minVal)/max(maxValminVal, 1e6)) or Qt.black 232 #self.shapes[i][discI]=self.graph.shapeList[0] 233 self.names[i][attrI]=check(data[i],attr) and " "+str(data[i][attr]) or "" 234 self.sizes[i][contI]=check(data[i],attr) and int(self.data[i][attr]/maxVal*9)+1 or 5 235 contI+=1 236 attrI+=1 330 if check(data[i], attr): 331 self.colors[i][attrI] = c.getColor((data[i][attr]  minVal) / span) 332 self.names[i][attrI] = " " + str(data[i][attr]) 333 self.sizes[i][contI] = int(self.data[i][attr] / maxVal * 9) + 1 334 else: 335 self.colors[i][attrI] = Qt.black 336 self.names[i][attrI] = "" 337 self.sizes[i][contI] = 5 338 contI += 1 339 attrI += 1 237 340 else: 238 341 for i in range(len(data)): 239 self.colors[i][attrI]=Qt.black 240 #self.shapes[i][j+1]=self.graph.shapeList[0] 241 self.names[i][attrI]= check(data[i],attr) and " "+str(data[i][attr]) or "" 242 #self.sizes[i][j+1]=5 243 attrI+=1 342 self.colors[i][attrI] = Qt.black 343 if check(data[i], attr): 344 self.names[i][attrI] = " " + str(data[i][attr]) 345 else: 346 self.names[i][attrI] = "" 347 348 attrI += 1 349 244 350 if data and data.domain.classVar: 245 351 if data.domain.classVar.varType == orange.VarTypes.Discrete: 246 self.graph.ColorAttr = len(self.colors[0])  1 # index 0 is Same color!352 self.graph.ColorAttr = len(self.colors[0])  1 # index 0 is Same color! 247 353 elif data.domain.classVar.varType == orange.VarTypes.Continuous: 248 self.graph.SizeAttr = len(self.sizes[0])  1 # index 0 is Same color!354 self.graph.SizeAttr = len(self.sizes[0])  1 # index 0 is Same color! 249 355 250 356 def setVarList(self, data): … … 257 363 for name in ["No name", "Var name"]: 258 364 self.nameCombo.addItem(name) 259 self.colors =[[Qt.black]*3 for i in range(len(data))]260 self.shapes =[[QwtSymbol.Ellipse] for i in range(len(data))]261 self.sizes =[[5] for i in range(len(data))]262 self.names =[[""]*4 for i in range(len(data))]263 self.selectedInput =[False]*len(data)365 self.colors = [[Qt.black] * 3 for i in range(len(data))] 366 self.shapes = [[QwtSymbol.Ellipse] for i in range(len(data))] 367 self.sizes = [[5] for i in range(len(data))] 368 self.names = [[""] * 4 for i in range(len(data))] 369 self.selectedInput = [False] * len(data) 264 370 try: 265 c =OWColorPalette.ColorPaletteHSV(len(data))371 c = OWColorPalette.ColorPaletteHSV(len(data)) 266 372 for i, d in enumerate(data): 267 self.colors[i][1] =c[i]268 self.names[i][1] =" " +str(d.name)373 self.colors[i][1] = c[i] 374 self.names[i][1] = " " + str(d.name) 269 375 except Exception, val: 270 376 print val 271 377 272 def updateStressBySize(self, noRepaint =False):378 def updateStressBySize(self, noRepaint=False): 273 379 self.sizeCombo.setDisabled(self.graph.stressBySize) 274 380 if not noRepaint: 275 381 self.graph.updateData() 276 382 277 383 def smacofStep(self): 278 384 if not getattr(self, "mds", None): … … 282 388 if self.computeStress: 283 389 self.mds.getStress(self.stressFunc[self.StressFunc][1]) 284 self.stress =self.getAvgStress(self.stressFunc[self.StressFunc][1])285 #st=time.clock() 390 self.stress = self.getAvgStress(self.stressFunc[self.StressFunc][1]) 391 286 392 if self.ReDraw: 287 393 self.graph.updateData() 288 #print "Update:", time.clock()st289 394 290 395 ## I (Janez) disabled LSMT because it is implemented as it never should be: … … 293 398 # user can "untransform" it, except for resending the signal 294 399 # Since the basic problem is in bad design of orngMDS, I removed the option 295 # from the widget. If somebody has time to fix orngMDS first, he's welcome. 400 # from the widget. If somebody has time to fix orngMDS first, he's welcome. 296 401 def LSMT(self): 297 402 if not getattr(self, "mds", None): … … 300 405 if self.computeStress: 301 406 self.mds.getStress(self.stressFunc[self.StressFunc][1]) 302 self.stress =self.getAvgStress(self.stressFunc[self.StressFunc][1])407 self.stress = self.getAvgStress(self.stressFunc[self.StressFunc][1]) 303 408 if self.ReDraw: 304 409 self.graph.updateData() … … 310 415 if self.computeStress: 311 416 self.mds.getStress(self.stressFunc[self.StressFunc][1]) 312 self.stress =self.getAvgStress(self.stressFunc[self.StressFunc][1])417 self.stress = self.getAvgStress(self.stressFunc[self.StressFunc][1]) 313 418 self.graph.updateData() 314 419 … … 316 421 if not getattr(self, "mds", None): 317 422 return 318 self.mds.points = numpy.random.random(size=[self.mds.n, 2])423 self.mds.points = numpy.random.random(size=[self.mds.n, 2]) 319 424 if self.computeStress: 320 425 self.mds.getStress(self.stressFunc[self.StressFunc][1]) 321 self.stress =self.getAvgStress(self.stressFunc[self.StressFunc][1])426 self.stress = self.getAvgStress(self.stressFunc[self.StressFunc][1]) 322 427 self.graph.updateData() 323 428 … … 330 435 for i in range(self.mds.n): 331 436 for j in range(2): 332 self.mds.points[i][j] += st[j] *(random()  0.5)437 self.mds.points[i][j] += st[j] * (random()  0.5) 333 438 if self.computeStress: 334 439 self.mds.getStress(self.stressFunc[self.StressFunc][1]) 335 self.stress =self.getAvgStress(self.stressFunc[self.StressFunc][1])440 self.stress = self.getAvgStress(self.stressFunc[self.StressFunc][1]) 336 441 self.graph.updateData() 337 338 def start(self):339 if not getattr(self, "mds", None):340 return341 if self.done==False:342 self.done=True343 return344 self.done=False345 self.startButton.setText("Stop")346 numIter=0347 self.progressBarInit()348 pcur=0349 startStress=oldStress=stress=self.getAvgStress(self.stressFunc[self.StressFunc][1])350 startTime=time.clock()351 hist=[stress]*3352 while not self.done and numIter<self.maxIterations:353 for i in range(self.NumIter):354 self.mds.SMACOFstep()355 qApp.processEvents()356 if self.computeStress:357 self.mds.getStress(self.stressFunc[self.StressFunc][1])358 self.stress=stress=self.getAvgStress(self.stressFunc[self.StressFunc][1])359 hist.pop(0)360 hist.append(abs(oldStressstress))361 numIter+=1362 self.infoB.setText("Num. steps: %i" % numIter)363 qApp.processEvents()364 if self.ReDraw:365 self.graph.updateData()366 qApp.processEvents()367 if self.computeStress and abs(sum(hist)/3)<abs(self.minStressDelta*oldStress):368 break369 ## Update progress bar370 p1=abs(self.minStressDelta*oldStress)/max(sum(hist)/3, 1e6)*100371 if p1>100: p1=0372 pcur=min(max([p1, float(numIter)/self.maxIterations*100, pcur]),99)373 self.progressBarSet(int(pcur))374 375 oldStress=stress376 self.startButton.setText("Optimize")377 self.progressBarFinished()378 #if not self.ReDraw:379 self.graph.updateData()380 self.done=True381 #print "time %i " % (time.clock()startTime)382 442 383 443 def testStart(self): 384 444 if not getattr(self, "mds", None): 385 445 return 386 if self.done ==False:387 self.done =True388 return 389 self.done =False446 if self.done == False: 447 self.done = True 448 return 449 self.done = False 390 450 self.startButton.setText("Stop Optimization") 391 451 self.stopping.setDisabled(1) 392 452 self.progressBarInit() 393 self.iterNum=0 394 self.mds.progressCallback=self.callback 395 # The name mangling for orange2.5 does not seem to work for orangeom.MDS 396 # so I set this explicitly 397 self.mds.progress_callback=self.callback 398 399 self.mds.mds.optimize(self.maxIterations, self.stressFunc[self.StressFunc][1], self.minStressDelta) 400 if self.iterNum%(math.pow(10,self.RefreshMode)): 453 self.iterNum = 0 454 self.mds.progressCallback = self.callback 455 # The name mangling for orange2.5 does not seem to work for 456 # orangeom.MDS so I set this explicitly 457 self.mds.progress_callback = self.callback 458 459 self.mds.mds.optimize(self.maxIterations, 460 self.stressFunc[self.StressFunc][1], 461 self.minStressDelta) 462 if self.iterNum % (math.pow(10, self.RefreshMode)): 401 463 self.graph.updateData() 402 464 self.startButton.setText("Optimize") 403 465 self.stopping.setDisabled(0) 404 466 self.progressBarFinished() 405 self.done =True406 407 def callback(self, a, b=None):408 if not self.iterNum %(math.pow(10,self.RefreshMode)):467 self.done = True 468 469 def callback(self, a, b=None): 470 if not self.iterNum % (math.pow(10, self.RefreshMode)): 409 471 self.graph.updateData() 410 self.iterNum +=1472 self.iterNum += 1 411 473 self.infoB.setText("Num. steps: %i" % self.iterNum) 412 474 self.infoA.setText("Avg. Stress: %f" % self.mds.avgStress) 413 self.progressBarSet(int(a*100)) 414 qApp.processEvents() 475 self.progressBarSet(int(a * 100)) 415 476 if self.done: 416 477 return 0 … … 418 479 return 1 419 480 420 421 481 def getAvgStress(self, stressf=orngMDS.SgnRelStress): 422 482 return self.mds.avgStress 423 """424 self.mds.getDistance()425 total=0.0426 total=sum([abs(a[0]) for a in self.mds.arr])427 self.infoA.setText("Avg. stress: %.7f" % (total/(self.mds.n*self.mds.n)))428 return total/(self.mds.n*self.mds.n)429 """430 483 431 484 def sendIf(self, i=1): 432 485 if self.autoSendSelection: 433 486 self.sendSelections() 434 487 435 488 def sendSelections(self): 436 489 if not getattr(self, "mds", None): 437 490 return 438 selectedInd =[]439 for i, (x,y) in enumerate(self.mds.points):440 if self.graph.isPointSelected(x, y):441 selectedInd +=[i]442 if type(self.data) ==orange.ExampleTable:491 selectedInd = [] 492 for i, (x, y) in enumerate(self.mds.points): 493 if self.graph.isPointSelected(x, y): 494 selectedInd += [i] 495 if type(self.data) == orange.ExampleTable: 443 496 self.sendExampleTable(selectedInd) 444 497 445 498 def sendExampleTable(self, selectedInd): 446 if self.selectionOptions ==0:499 if self.selectionOptions == 0: 447 500 self.send("Data", orange.ExampleTable(self.data.getitems(selectedInd))) 448 501 else: 449 xAttr=orange.FloatVariable("X") 450 yAttr=orange.FloatVariable("Y") 451 if self.selectionOptions==1: 452 domain=orange.Domain([xAttr, yAttr]+[v for v in self.data.domain.variables]) 502 xAttr = orange.FloatVariable("X") 503 yAttr = orange.FloatVariable("Y") 504 if self.selectionOptions == 1: 505 domain = orange.Domain([xAttr, yAttr] + 506 [v for v in self.data.domain.variables]) 453 507 domain.addmetas(self.data.domain.getmetas()) 454 508 else: 455 domain =orange.Domain(self.data.domain)509 domain = orange.Domain(self.data.domain) 456 510 domain.addmeta(orange.newmetaid(), xAttr) 457 511 domain.addmeta(orange.newmetaid(), yAttr) 458 selection =orange.ExampleTable(domain)512 selection = orange.ExampleTable(domain) 459 513 selection.extend(self.data.getitems(selectedInd)) 460 514 for i in range(len(selectedInd)): 461 selection[i][xAttr] =self.mds.points[selectedInd[i]][0]462 selection[i][yAttr] =self.mds.points[selectedInd[i]][1]515 selection[i][xAttr] = self.mds.points[selectedInd[i]][0] 516 selection[i][yAttr] = self.mds.points[selectedInd[i]][1] 463 517 self.send("Data", selection) 464 518 … … 470 524 471 525 def sendReport(self): 472 self.reportSettings("Optimization", 473 [("Stress function", self.stressFunc[self.StressFunc][0]), 474 ("Minimal stress change", self.minStressDelta), 475 ("Maximal number of steps", self.maxIterations)]) 476 if self.graph.ColorAttr or self.graph.stressBySize or self.graph.SizeAttr or self.graph.ShapeAttr or self.graph.NameAttr or self.graph.ShowStress: 477 self.reportSettings("Visual settings", 478 [self.graph.ColorAttr and ("Point color", self.colorCombo.currentText()), 479 self.graph.stressBySize and ("Point size", "<stress>") 480 or self.graph.SizeAttr and ("Point size", self.sizeCombo.currentText()), 481 self.graph.ShapeAttr and ("Point shape", self.shapeCombo.currentText()), 482 self.graph.NameAttr and ("Labels", self.nameCombo.currentText()), 483 self.graph.ShowStress and ("Proportion of connected pairs", self.graph.proportionGraphed)]) 526 self.reportSettings( 527 "Optimization", 528 [("Stress function", self.stressFunc[self.StressFunc][0]), 529 ("Minimal stress change", self.minStressDelta), 530 ("Maximal number of steps", self.maxIterations)] 531 ) 532 533 if self.graph.ColorAttr or self.graph.stressBySize or \ 534 self.graph.SizeAttr or self.graph.ShapeAttr or \ 535 self.graph.NameAttr or self.graph.ShowStress: 536 self.reportSettings( 537 "Visual settings", 538 [self.graph.ColorAttr and ("Point color", self.colorCombo.currentText()), 539 self.graph.stressBySize and ("Point size", "<stress>") 540 or self.graph.SizeAttr and ("Point size", self.sizeCombo.currentText()), 541 self.graph.ShapeAttr and ("Point shape", self.shapeCombo.currentText()), 542 self.graph.NameAttr and ("Labels", self.nameCombo.currentText()), 543 self.graph.ShowStress and ("Proportion of connected pairs", self.graph.proportionGraphed)] 544 ) 545 484 546 self.reportSection("Chart") 485 547 self.reportImage(self.graph.saveToFileDirect) 486 548 487 549 488 550 class MDSGraph(OWGraph): … … 503 565 self.proportionGraphed = 20 504 566 self.ShowName = True 505 # self.curveKeys=[]567 # self.curveKeys=[] 506 568 self.pointKeys = [] 507 569 self.points = [] … … 513 575 self.closestPairs = None 514 576 self.shapeList = [QwtSymbol.Ellipse, 515 516 517 518 QwtSymbol.DTriangle,519 520 521 522 523 QwtSymbol.XCross]524 577 QwtSymbol.Rect, 578 QwtSymbol.Diamond, 579 QwtSymbol.Triangle, 580 QwtSymbol.DTriangle, 581 QwtSymbol.UTriangle, 582 QwtSymbol.LTriangle, 583 QwtSymbol.RTriangle, 584 QwtSymbol.Cross, 585 QwtSymbol.XCross] 586 525 587 self.axis_margin = 10 526 588 527 589 def setData(self, mds, colors, sizes, shapes, names, showFilled): 528 590 self.mds = mds 529 # self.data=data591 # self.data=data 530 592 self.colors = colors 531 593 self.sizes = sizes 532 594 self.shapes = shapes 533 595 self.names = names 534 self.showFilled = showFilled #map(lambda d: not d, showFilled)596 self.showFilled = showFilled # map(lambda d: not d, showFilled) 535 597 self.updateData() 536 598 … … 548 610 return 549 611 N = len(self.mds.points) 550 np = min(int(N*(N1)/2. * self.proportionGraphed/100.), 1000) # draw maximum of 1000 closest pairs 551 needlines = int(math.ceil((1 + math.sqrt(1+8*np)) / 2)) 612 # draw maximum of 1000 closest pairs 613 np = min(int(N * (N  1) / 2. * self.proportionGraphed / 100.), 1000) 614 needlines = int(math.ceil((1 + math.sqrt(1 + 8 * np)) / 2)) 552 615 553 616 if self.closestPairs is None or len(self.closestPairs) < np: … … 555 618 m = self.mds.originalDistances 556 619 self.closestPairs = sorted(heapq.nsmallest(np, ((m[i, j], i, j) for i in range(m.dim) for j in range(i)))) 557 620 558 621 for c in self.distanceLineKeys: 559 622 try: 560 623 c.detach() 561 except RuntimeError, ex: #underlying C/C++ object has been deleted 624 except RuntimeError: 625 # underlying C/C++ object has been deleted 562 626 pass 563 627 self.distanceLineKeys = [] 564 628 565 629 hdist = self.closestPairs[:np] 566 630 if not hdist: 567 631 return 568 569 black = QColor(192, 192,192)632 633 black = QColor(192, 192, 192) 570 634 if self.differentWidths: 571 635 mindist = hdist[0][0] … … 574 638 mindist = maxdist = 0 575 639 if maxdist != mindist: 576 k = 3 / (maxdist  mindist) **2640 k = 3 / (maxdist  mindist) ** 2 577 641 for dist, i, j in hdist: 578 642 pti, ptj = self.mds.points[i], self.mds.points[j] 579 c = self.addCurve("A", black, black, 0, QwtPlotCurve.Lines, xData=[pti[0],ptj[0]], yData=[pti[1],ptj[1]], lineWidth = max(1, (maxdist  dist)**2 * k)) 643 c = self.addCurve("A", black, black, 0, QwtPlotCurve.Lines, 644 xData=[pti[0], ptj[0]], yData=[pti[1], ptj[1]], 645 lineWidth=max(1, (maxdist  dist) ** 2 * k)) 580 646 c.setZ(10) 581 647 self.distanceLineKeys.append(c) … … 583 649 for dist, i, j in hdist: 584 650 pti, ptj = self.mds.points[i], self.mds.points[j] 585 c = self.addCurve("A", black, black, 0, QwtPlotCurve.Lines, xData=[pti[0],ptj[0]], yData=[pti[1],ptj[1]], lineWidth = 2) 651 c = self.addCurve("A", black, black, 0, QwtPlotCurve.Lines, 652 xData=[pti[0], ptj[0]], yData=[pti[1], ptj[1]], 653 lineWidth=2) 586 654 c.setZ(10) 587 655 self.distanceLineKeys.append(c) 588 589 656 590 657 def updateLinesRepaint(self): 591 658 if self.mds: … … 596 663 try: 597 664 c.detach() 598 except RuntimeError, ex: #underlying C/C++ object has been deleted 599 pass 665 except RuntimeError: 666 # underlying C/C++ object has been deleted 667 pass 600 668 self.distanceLineKeys = [] 601 669 self.replot() … … 603 671 def setPoints(self): 604 672 if not self.mds: 605 return 606 if self.ShapeAttr==0 and self.SizeAttr==0 and self.NameAttr==0 and not self.stressBySize and not self.stressByTransparency: 607 colors=[c[self.ColorAttr] for c in self.colors] 608 609 set=[] 673 return 674 if self.ShapeAttr == 0 and self.SizeAttr == 0 and \ 675 self.NameAttr == 0 and not self.stressBySize and \ 676 not self.stressByTransparency: 677 colors = [c[self.ColorAttr] for c in self.colors] 678 679 set = [] 610 680 for c in colors: 611 if 681 if c not in set: 612 682 set.append(c) 613 #set=reduce(lambda set,a: (not(a in set)) and set.append(a), colors, []) 614 #set=sets.ImmutableSet([c[self.ColorAttr] for c in self.colors]) 615 616 dict={} 683 684 dict = {} 617 685 for i in range(len(self.colors)): 618 686 hsv = QColor(self.colors[i][self.ColorAttr]).getHsv() … … 620 688 dict[hsv].append(i) 621 689 else: 622 dict[hsv]=[i] 623 # maxX, maxY = self.mds.points[0] if len(self.mds.points)>0 else (0, 0) 624 # minX, minY = self.mds.points[0] if len(self.mds.points)>0 else (0, 0) 690 dict[hsv] = [i] 691 625 692 for color in set: 626 #print len(dict[color.getHsv()]), color.name() 627 X=[self.mds.points[i][0] for i in dict[QColor(color).getHsv()] if self.showFilled[i]] 628 Y=[self.mds.points[i][1] for i in dict[QColor(color).getHsv()] if self.showFilled[i]] 629 c = self.addCurve("A", color, color, self.PointSize, symbol=QwtSymbol.Ellipse, xData=X, yData=Y) 693 X = [self.mds.points[i][0] for i in dict[QColor(color).getHsv()] 694 if self.showFilled[i]] 695 Y = [self.mds.points[i][1] for i in dict[QColor(color).getHsv()] 696 if self.showFilled[i]] 697 c = self.addCurve("A", color, color, self.PointSize, 698 symbol=QwtSymbol.Ellipse, xData=X, yData=Y) 630 699 c.setZ(100) 631 632 X=[self.mds.points[i][0] for i in dict[QColor(color).getHsv()] if not self.showFilled[i]] 633 Y=[self.mds.points[i][1] for i in dict[QColor(color).getHsv()] if not self.showFilled[i]] 634 c = self.addCurve("A", color, color, self.PointSize, symbol=QwtSymbol.Ellipse, xData=X, yData=Y, showFilledSymbols=False) 700 701 X = [self.mds.points[i][0] for i in dict[QColor(color).getHsv()] 702 if not self.showFilled[i]] 703 Y = [self.mds.points[i][1] for i in dict[QColor(color).getHsv()] 704 if not self.showFilled[i]] 705 c = self.addCurve("A", color, color, self.PointSize, 706 symbol=QwtSymbol.Ellipse, xData=X, yData=Y, 707 showFilledSymbols=False) 635 708 c.setZ(100) 636 709 else: … … 638 711 stresses = map(sum, self.mds.stress) 639 712 mins, maxs = min(stresses), max(stresses) 640 ks = self.PointSize / max(1, maxs mins)641 cs = 1 / max(1., maxs mins)713 ks = self.PointSize / max(1, maxs  mins) 714 cs = 1 / max(1., maxs  mins) 642 715 for i in range(len(self.colors)): 643 716 cq = QColor(self.colors[i][self.ColorAttr]) … … 645 718 cq.setAlpha(255 * (1  cs * (stresses[i]  mins))) 646 719 c = self.addCurve("a", cq, self.colors[i][self.ColorAttr], 647 max(5, ks*(1 + maxs  stresses[i])) if self.stressBySize else self.sizes[i][self.SizeAttr]*1.0/5*self.PointSize, 648 symbol=self.shapes[i][self.ShapeAttr], xData=[self.mds.points[i][0]],yData=[self.mds.points[i][1]], showFilledSymbols=self.showFilled[i]) 720 (max(5, ks * (1 + maxs  stresses[i])) 721 if self.stressBySize else 722 self.sizes[i][self.SizeAttr] * 1.0 / 5 * self.PointSize), 723 symbol=self.shapes[i][self.ShapeAttr], 724 xData=[self.mds.points[i][0]], 725 yData=[self.mds.points[i][1]], 726 showFilledSymbols=self.showFilled[i]) 727 649 728 c.setZ(100) 650 if self.NameAttr!=0: 651 c = self.addMarker(self.names[i][self.NameAttr], self.mds.points[i][0], self.mds.points[i][1], Qt.AlignBottom) 729 if self.NameAttr != 0: 730 c = self.addMarker(self.names[i][self.NameAttr], 731 self.mds.points[i][0], 732 self.mds.points[i][1], 733 Qt.AlignBottom) 652 734 c.setZ(100) 653 735 654 # for i in range(len(self.colors)): 655 # c = self.addCurve("a", self.colors[i][self.ColorAttr], self.colors[i][self.ColorAttr], self.sizes[i][self.SizeAttr]*1.0/5*self.PointSize, 656 # symbol=self.shapes[i][self.ShapeAttr], xData=[self.mds.points[i][0]],yData=[self.mds.points[i][1]], showFilledSymbols=self.showFilled[i]) 657 # c.setZ(100) 658 # if self.NameAttr!=0: 659 # c = self.addMarker(self.names[i][self.NameAttr], self.mds.points[i][0], self.mds.points[i][1], Qt.AlignRight) 660 # c.setZ(100) 661 662 663 if len(self.mds.points)>0: 736 if len(self.mds.points) > 0: 664 737 X = [point[0] for point in self.mds.points] 665 738 Y = [point[1] for point in self.mds.points] … … 668 741 span_x = max_x  min_x 669 742 span_y = max_y  min_y 670 self.setAxisScale(QwtPlot.xBottom, min_x  0.05 * span_x, max_x + 0.05 * span_x) 671 self.setAxisScale(QwtPlot.yLeft, min_y  0.05 * span_y, max_y + 0.05 * span_y) 672 743 self.setAxisScale(QwtPlot.xBottom, 744 min_x  0.05 * span_x, max_x + 0.05 * span_x) 745 self.setAxisScale(QwtPlot.yLeft, 746 min_y  0.05 * span_y, max_y + 0.05 * span_y) 673 747 674 748 def sendData(self, *args): 675 749 pass 676 750 677 if __name__=="__main__": 678 app=QApplication(sys.argv) 679 w=OWMDS() 751 752 if __name__ == "__main__": 753 app = QApplication(sys.argv) 754 w = OWMDS() 680 755 w.show() 681 data=orange.ExampleTable("../../doc/datasets/iris.tab") 682 ## data = orange.ExampleTable(r"E:\Development\Orange Datasets\UCI\iris.tab") 683 ## data=orange.ExampleTable("/home/ales/src/MDSjakulin/eu_nations.txt") 756 data = orange.ExampleTable("iris") 757 684 758 matrix = orange.SymMatrix(len(data)) 685 759 dist = orange.ExamplesDistanceConstructor_Euclidean(data) … … 687 761 matrix.setattr('items', data) 688 762 for i in range(len(data)): 689 for j in range(i +1):763 for j in range(i + 1): 690 764 matrix[i, j] = dist(data[i], data[j]) 691 765 692 766 w.cmatrix(matrix) 693 767 app.exec_() 694
Note: See TracChangeset
for help on using the changeset viewer.