| 1 | """ |
|---|
| 2 | <name>Data Table</name> |
|---|
| 3 | <description>Shows data in a spreadsheet.</description> |
|---|
| 4 | <icon>icons/DataTable.png</icon> |
|---|
| 5 | <priority>100</priority> |
|---|
| 6 | <contact>Peter Juvan (peter.juvan@fri.uni-lj.si)</contact> |
|---|
| 7 | """ |
|---|
| 8 | |
|---|
| 9 | # OWDataTable.py |
|---|
| 10 | # |
|---|
| 11 | # wishes: |
|---|
| 12 | # ignore attributes, filter examples by attribute values, do |
|---|
| 13 | # all sorts of preprocessing (including discretization) on the table, |
|---|
| 14 | # output a new table and export it in variety of formats. |
|---|
| 15 | |
|---|
| 16 | from OWWidget import * |
|---|
| 17 | import OWGUI |
|---|
| 18 | import math |
|---|
| 19 | from orngDataCaching import * |
|---|
| 20 | |
|---|
| 21 | ############################################################################## |
|---|
| 22 | |
|---|
| 23 | OrangeValueRole = Qt.UserRole + 1 |
|---|
| 24 | |
|---|
| 25 | class OWDataTable(OWWidget): |
|---|
| 26 | settingsList = ["showDistributions", "showMeta", "distColorRgb", "showAttributeLabels"] |
|---|
| 27 | |
|---|
| 28 | def __init__(self, parent=None, signalManager = None): |
|---|
| 29 | OWWidget.__init__(self, parent, signalManager, "Data Table") |
|---|
| 30 | |
|---|
| 31 | self.inputs = [("Examples", ExampleTable, self.dataset, Multiple + Default)] |
|---|
| 32 | self.outputs = [] |
|---|
| 33 | |
|---|
| 34 | self.data = {} # key: id, value: ExampleTable |
|---|
| 35 | self.showMetas = {} # key: id, value: (True/False, columnList) |
|---|
| 36 | self.showMeta = 1 |
|---|
| 37 | self.showAttributeLabels = 1 |
|---|
| 38 | self.showDistributions = 1 |
|---|
| 39 | self.distColorRgb = (220,220,220, 255) |
|---|
| 40 | self.distColor = QColor(*self.distColorRgb) |
|---|
| 41 | self.locale = QLocale() |
|---|
| 42 | |
|---|
| 43 | self.loadSettings() |
|---|
| 44 | |
|---|
| 45 | # info box |
|---|
| 46 | infoBox = OWGUI.widgetBox(self.controlArea, "Info") |
|---|
| 47 | self.infoEx = OWGUI.widgetLabel(infoBox, 'No data on input.') |
|---|
| 48 | self.infoMiss = OWGUI.widgetLabel(infoBox, ' ') |
|---|
| 49 | OWGUI.widgetLabel(infoBox, ' ') |
|---|
| 50 | self.infoAttr = OWGUI.widgetLabel(infoBox, ' ') |
|---|
| 51 | self.infoMeta = OWGUI.widgetLabel(infoBox, ' ') |
|---|
| 52 | OWGUI.widgetLabel(infoBox, ' ') |
|---|
| 53 | self.infoClass = OWGUI.widgetLabel(infoBox, ' ') |
|---|
| 54 | infoBox.setMinimumWidth(200) |
|---|
| 55 | OWGUI.separator(self.controlArea) |
|---|
| 56 | |
|---|
| 57 | # settings box |
|---|
| 58 | boxSettings = OWGUI.widgetBox(self.controlArea, "Settings") |
|---|
| 59 | self.cbShowMeta = OWGUI.checkBox(boxSettings, self, "showMeta", 'Show meta attributes', callback = self.cbShowMetaClicked) |
|---|
| 60 | self.cbShowMeta.setEnabled(False) |
|---|
| 61 | self.cbShowAttLbls = OWGUI.checkBox(boxSettings, self, "showAttributeLabels", 'Show attribute labels (if any)', callback = self.cbShowAttLabelsClicked) |
|---|
| 62 | self.cbShowAttLbls.setEnabled(True) |
|---|
| 63 | self.cbShowDistributions = OWGUI.checkBox(boxSettings, self, "showDistributions", 'Visualize continuous values', callback = self.cbShowDistributions) |
|---|
| 64 | colBox = OWGUI.indentedBox(boxSettings, orientation = "horizontal") |
|---|
| 65 | OWGUI.widgetLabel(colBox, "Color: ") |
|---|
| 66 | self.colButton = OWGUI.toolButton(colBox, self, self.changeColor, width=20, height=20, debuggingEnabled = 0) |
|---|
| 67 | OWGUI.rubber(colBox) |
|---|
| 68 | |
|---|
| 69 | resizeColsBox = OWGUI.widgetBox(boxSettings, 0, "horizontal", 0) |
|---|
| 70 | OWGUI.label(resizeColsBox, self, "Resize columns: ") |
|---|
| 71 | OWGUI.button(resizeColsBox, self, "+", self.increaseColWidth, tooltip = "Increase the width of the columns", width=30) |
|---|
| 72 | OWGUI.button(resizeColsBox, self, "-", self.decreaseColWidth, tooltip = "Decrease the width of the columns", width=30) |
|---|
| 73 | OWGUI.rubber(resizeColsBox) |
|---|
| 74 | |
|---|
| 75 | self.btnResetSort = OWGUI.button(boxSettings, self, "Restore Order of Examples", callback = self.btnResetSortClicked, tooltip = "Show examples in the same order as they appear in the file") |
|---|
| 76 | |
|---|
| 77 | OWGUI.rubber(self.controlArea) |
|---|
| 78 | |
|---|
| 79 | # GUI with tabs |
|---|
| 80 | self.tabs = OWGUI.tabWidget(self.mainArea) |
|---|
| 81 | self.id2table = {} # key: widget id, value: table |
|---|
| 82 | self.table2id = {} # key: table, value: widget id |
|---|
| 83 | self.connect(self.tabs,SIGNAL("currentChanged(QWidget*)"),self.tabClicked) |
|---|
| 84 | |
|---|
| 85 | self.updateColor() |
|---|
| 86 | |
|---|
| 87 | def changeColor(self): |
|---|
| 88 | color = QColorDialog.getColor(self.distColor, self) |
|---|
| 89 | if color.isValid(): |
|---|
| 90 | self.distColorRgb = color.getRgb() |
|---|
| 91 | self.updateColor() |
|---|
| 92 | |
|---|
| 93 | def updateColor(self): |
|---|
| 94 | self.distColor = QColor(*self.distColorRgb) |
|---|
| 95 | w = self.colButton.width()-8 |
|---|
| 96 | h = self.colButton.height()-8 |
|---|
| 97 | pixmap = QPixmap(w, h) |
|---|
| 98 | painter = QPainter() |
|---|
| 99 | painter.begin(pixmap) |
|---|
| 100 | painter.fillRect(0,0,w,h, QBrush(self.distColor)) |
|---|
| 101 | painter.end() |
|---|
| 102 | self.colButton.setIcon(QIcon(pixmap)) |
|---|
| 103 | |
|---|
| 104 | def increaseColWidth(self): |
|---|
| 105 | table = self.tabs.currentWidget() |
|---|
| 106 | for col in range(table.columnCount()): |
|---|
| 107 | w = table.columnWidth(col) |
|---|
| 108 | table.setColumnWidth(col, w + 10) |
|---|
| 109 | |
|---|
| 110 | def decreaseColWidth(self): |
|---|
| 111 | table = self.tabs.currentWidget() |
|---|
| 112 | for col in range(table.columnCount()): |
|---|
| 113 | w = table.columnWidth(col) |
|---|
| 114 | minW = table.sizeHintForColumn(col) |
|---|
| 115 | table.setColumnWidth(col, max(w - 10, minW)) |
|---|
| 116 | |
|---|
| 117 | |
|---|
| 118 | def dataset(self, data, id=None): |
|---|
| 119 | """Generates a new table and adds it to a new tab when new data arrives; |
|---|
| 120 | or hides the table and removes a tab when data==None; |
|---|
| 121 | or replaces the table when new data arrives together with already existing id.""" |
|---|
| 122 | if data != None: # can be an empty table! |
|---|
| 123 | if self.data.has_key(id): |
|---|
| 124 | # remove existing table |
|---|
| 125 | self.data.pop(id) |
|---|
| 126 | self.showMetas.pop(id) |
|---|
| 127 | self.id2table[id].hide() |
|---|
| 128 | self.tabs.removeTab(self.tabs.indexOf(self.id2table[id])) |
|---|
| 129 | self.table2id.pop(self.id2table.pop(id)) |
|---|
| 130 | self.data[id] = data |
|---|
| 131 | self.showMetas[id] = (True, []) |
|---|
| 132 | |
|---|
| 133 | table = OWGUI.table(None, 0,0) |
|---|
| 134 | table.setSelectionBehavior(QAbstractItemView.SelectRows) |
|---|
| 135 | |
|---|
| 136 | self.id2table[id] = table |
|---|
| 137 | self.table2id[table] = id |
|---|
| 138 | if data.name: |
|---|
| 139 | tabName = "%s " % data.name |
|---|
| 140 | else: |
|---|
| 141 | tabName = "" |
|---|
| 142 | tabName += "(" + str(id[1]) + ")" |
|---|
| 143 | if id[2] != None: |
|---|
| 144 | tabName += " [" + str(id[2]) + "]" |
|---|
| 145 | self.tabs.addTab(table, tabName) |
|---|
| 146 | |
|---|
| 147 | self.progressBarInit() |
|---|
| 148 | self.setTable(table, data) |
|---|
| 149 | self.progressBarFinished() |
|---|
| 150 | self.tabs.setCurrentIndex(self.tabs.indexOf(table)) |
|---|
| 151 | self.setInfo(data) |
|---|
| 152 | self.cbShowMeta.setEnabled(len(self.showMetas[id][1])>0) # enable showMetas checkbox only if metas exist |
|---|
| 153 | |
|---|
| 154 | elif self.data.has_key(id): |
|---|
| 155 | table = self.id2table[id] |
|---|
| 156 | self.data.pop(id) |
|---|
| 157 | self.showMetas.pop(id) |
|---|
| 158 | table.hide() |
|---|
| 159 | self.tabs.removeTab(self.tabs.indexOf(table)) |
|---|
| 160 | self.table2id.pop(self.id2table.pop(id)) |
|---|
| 161 | self.setInfo(self.data.get(self.table2id.get(self.tabs.currentWidget(),None),None)) |
|---|
| 162 | |
|---|
| 163 | # disable showMetas checkbox if there is no data on input |
|---|
| 164 | if len(self.data) == 0: |
|---|
| 165 | self.cbShowMeta.setEnabled(False) |
|---|
| 166 | |
|---|
| 167 | # Writes data into table, adjusts the column width. |
|---|
| 168 | def setTable(self, table, data): |
|---|
| 169 | if data==None: |
|---|
| 170 | return |
|---|
| 171 | qApp.setOverrideCursor(Qt.WaitCursor) |
|---|
| 172 | vars = data.domain.variables |
|---|
| 173 | m = data.domain.getmetas(False) |
|---|
| 174 | ml = [(k, m[k]) for k in m] |
|---|
| 175 | ml.sort(lambda x,y: cmp(y[0], x[0])) |
|---|
| 176 | metas = [x[1] for x in ml] |
|---|
| 177 | metaKeys = [x[0] for x in ml] |
|---|
| 178 | |
|---|
| 179 | mo = data.domain.getmetas(True).items() |
|---|
| 180 | if mo: |
|---|
| 181 | mo.sort(lambda x,y: cmp(x[1].name.lower(),y[1].name.lower())) |
|---|
| 182 | # metas.append(None) |
|---|
| 183 | # metaKeys.append(None) |
|---|
| 184 | |
|---|
| 185 | varsMetas = vars + metas |
|---|
| 186 | |
|---|
| 187 | numVars = len(data.domain.variables) |
|---|
| 188 | numMetas = len(metas) |
|---|
| 189 | numVarsMetas = numVars + numMetas |
|---|
| 190 | numEx = len(data) |
|---|
| 191 | numSpaces = int(math.log(max(numEx,1), 10))+1 |
|---|
| 192 | |
|---|
| 193 | table.clear() |
|---|
| 194 | table.oldSortingIndex = -1 |
|---|
| 195 | table.oldSortingOrder = 1 |
|---|
| 196 | table.setColumnCount(numVarsMetas) |
|---|
| 197 | table.setRowCount(numEx) |
|---|
| 198 | |
|---|
| 199 | table.dist = getCached(data, orange.DomainBasicAttrStat, (data,)) |
|---|
| 200 | |
|---|
| 201 | table.setItemDelegate(TableItemDelegate(self, table)) |
|---|
| 202 | table.variableNames = [var.name for var in varsMetas] |
|---|
| 203 | table.data = data |
|---|
| 204 | id = self.table2id.get(table, None) |
|---|
| 205 | |
|---|
| 206 | # set the header (attribute names) |
|---|
| 207 | table.setHorizontalHeaderLabels(table.variableNames) |
|---|
| 208 | if self.showAttributeLabels: |
|---|
| 209 | table.setHorizontalHeaderLabels([table.variableNames[i] + ("\n%s" % a.group if hasattr(a, "group") else "") for (i, a) in enumerate(table.data.domain.attributes)]) |
|---|
| 210 | |
|---|
| 211 | #table.hide() |
|---|
| 212 | clsColor = QColor(160,160,160) |
|---|
| 213 | metaColor = QColor(220,220,200) |
|---|
| 214 | white = QColor(Qt.white) |
|---|
| 215 | for j,(key,attr) in enumerate(zip(range(numVars) + metaKeys, varsMetas)): |
|---|
| 216 | self.progressBarSet(j*100.0/numVarsMetas) |
|---|
| 217 | if attr == data.domain.classVar: |
|---|
| 218 | bgColor = clsColor |
|---|
| 219 | elif attr in metas or attr is None: |
|---|
| 220 | bgColor = metaColor |
|---|
| 221 | self.showMetas[id][1].append(j) # store indices of meta attributes |
|---|
| 222 | else: |
|---|
| 223 | bgColor = white |
|---|
| 224 | |
|---|
| 225 | for i in range(numEx): |
|---|
| 226 | ## table.setItem(i, j, TableWidgetItem(data[i][key] |
|---|
| 227 | ## OWGUI.tableItem(table, i,j, str(data[i][key]), backColor = bgColor) |
|---|
| 228 | if data.domain[key].varType == orange.VarTypes.Continuous: |
|---|
| 229 | item = OWGUI.tableItem(table, i,j, float(str(data[i][key])), backColor = bgColor) |
|---|
| 230 | else: |
|---|
| 231 | item = OWGUI.tableItem(table, i,j, str(data[i][key]), backColor = bgColor) |
|---|
| 232 | ## item.setData(OrangeValueRole, QVariant(str(data[i][key]))) |
|---|
| 233 | |
|---|
| 234 | table.resizeRowsToContents() |
|---|
| 235 | table.resizeColumnsToContents() |
|---|
| 236 | |
|---|
| 237 | self.connect(table.horizontalHeader(), SIGNAL("sectionClicked(int)"), self.sortByColumn) |
|---|
| 238 | #table.verticalHeader().setMovable(False) |
|---|
| 239 | |
|---|
| 240 | qApp.restoreOverrideCursor() |
|---|
| 241 | #table.setCurrentCell(-1,-1) |
|---|
| 242 | #table.show() |
|---|
| 243 | |
|---|
| 244 | |
|---|
| 245 | def sortByColumn(self, index): |
|---|
| 246 | table = self.tabs.currentWidget() |
|---|
| 247 | table.horizontalHeader().setSortIndicatorShown(1) |
|---|
| 248 | header = table.horizontalHeader() |
|---|
| 249 | if index == table.oldSortingIndex: |
|---|
| 250 | order = table.oldSortingOrder == Qt.AscendingOrder and Qt.DescendingOrder or Qt.AscendingOrder |
|---|
| 251 | else: |
|---|
| 252 | order = Qt.AscendingOrder |
|---|
| 253 | table.sortByColumn(index, order) |
|---|
| 254 | table.oldSortingIndex = index |
|---|
| 255 | table.oldSortingOrder = order |
|---|
| 256 | #header.setSortIndicator(index, order) |
|---|
| 257 | |
|---|
| 258 | def tabClicked(self, qTableInstance): |
|---|
| 259 | """Updates the info box and showMetas checkbox when a tab is clicked. |
|---|
| 260 | """ |
|---|
| 261 | id = self.table2id.get(qTableInstance,None) |
|---|
| 262 | self.setInfo(self.data.get(id,None)) |
|---|
| 263 | show_col = self.showMetas.get(id,None) |
|---|
| 264 | if show_col: |
|---|
| 265 | self.cbShowMeta.setChecked(show_col[0]) |
|---|
| 266 | self.cbShowMeta.setEnabled(len(show_col[1])>0) |
|---|
| 267 | |
|---|
| 268 | def cbShowMetaClicked(self): |
|---|
| 269 | table = self.tabs.currentWidget() |
|---|
| 270 | id = self.table2id.get(table, None) |
|---|
| 271 | if self.showMetas.has_key(id): |
|---|
| 272 | show,col = self.showMetas[id] |
|---|
| 273 | self.showMetas[id] = (not show,col) |
|---|
| 274 | if show: |
|---|
| 275 | for c in col: |
|---|
| 276 | table.hideColumn(c) |
|---|
| 277 | else: |
|---|
| 278 | for c in col: |
|---|
| 279 | table.showColumn(c) |
|---|
| 280 | table.resizeColumnToContents(c) |
|---|
| 281 | |
|---|
| 282 | def cbShowAttLabelsClicked(self): |
|---|
| 283 | for table in self.table2id.keys(): |
|---|
| 284 | if self.showAttributeLabels: |
|---|
| 285 | table.setHorizontalHeaderLabels([table.variableNames[i] + ("\n%s" % a.group if hasattr(a, "group") else "") for (i, a) in enumerate(table.data.domain.attributes)]) |
|---|
| 286 | else: |
|---|
| 287 | table.setHorizontalHeaderLabels(table.variableNames) |
|---|
| 288 | # h = table.horizontalHeader().adjustSize() |
|---|
| 289 | |
|---|
| 290 | def cbShowDistributions(self): |
|---|
| 291 | table = self.tabs.currentWidget() |
|---|
| 292 | table.reset() |
|---|
| 293 | |
|---|
| 294 | # show data in the default order |
|---|
| 295 | def btnResetSortClicked(self): |
|---|
| 296 | table = self.tabs.currentWidget() |
|---|
| 297 | id = self.table2id[table] |
|---|
| 298 | data = self.data[id] |
|---|
| 299 | self.progressBarInit() |
|---|
| 300 | self.setTable(table, data) |
|---|
| 301 | self.progressBarFinished() |
|---|
| 302 | |
|---|
| 303 | def setInfo(self, data): |
|---|
| 304 | """Updates data info. |
|---|
| 305 | """ |
|---|
| 306 | def sp(l, capitalize=False): |
|---|
| 307 | n = len(l) |
|---|
| 308 | if n == 0: |
|---|
| 309 | if capitalize: |
|---|
| 310 | return "No", "s" |
|---|
| 311 | else: |
|---|
| 312 | return "no", "s" |
|---|
| 313 | elif n == 1: |
|---|
| 314 | return str(n), '' |
|---|
| 315 | else: |
|---|
| 316 | return str(n), 's' |
|---|
| 317 | |
|---|
| 318 | if data == None: |
|---|
| 319 | self.infoEx.setText('No data on input.') |
|---|
| 320 | self.infoMiss.setText('') |
|---|
| 321 | self.infoAttr.setText('') |
|---|
| 322 | self.infoMeta.setText('') |
|---|
| 323 | self.infoClass.setText('') |
|---|
| 324 | else: |
|---|
| 325 | self.infoEx.setText("%s example%s," % sp(data)) |
|---|
| 326 | missData = orange.Preprocessor_takeMissing(data) |
|---|
| 327 | self.infoMiss.setText('%s (%.1f%s) with missing values.' % (len(missData), len(data) and 100.*len(missData)/len(data), "%")) |
|---|
| 328 | self.infoAttr.setText("%s attribute%s," % sp(data.domain.attributes,True)) |
|---|
| 329 | self.infoMeta.setText("%s meta attribute%s." % sp(data.domain.getmetas())) |
|---|
| 330 | if data.domain.classVar: |
|---|
| 331 | if data.domain.classVar.varType == orange.VarTypes.Discrete: |
|---|
| 332 | self.infoClass.setText('Discrete class with %s value%s.' % sp(data.domain.classVar.values)) |
|---|
| 333 | elif data.domain.classVar.varType == orange.VarTypes.Continuous: |
|---|
| 334 | self.infoClass.setText('Continuous class.') |
|---|
| 335 | else: |
|---|
| 336 | self.infoClass.setText("Class is neither discrete nor continuous.") |
|---|
| 337 | else: |
|---|
| 338 | self.infoClass.setText('Classless domain.') |
|---|
| 339 | |
|---|
| 340 | |
|---|
| 341 | class TableItemDelegate(QItemDelegate): |
|---|
| 342 | def __init__(self, widget = None, table = None): |
|---|
| 343 | QItemDelegate.__init__(self, widget) |
|---|
| 344 | self.table = table |
|---|
| 345 | self.widget = widget |
|---|
| 346 | |
|---|
| 347 | def paint(self, painter, option, index): |
|---|
| 348 | painter.save() |
|---|
| 349 | self.drawBackground(painter, option, index) |
|---|
| 350 | value, ok = index.data(Qt.DisplayRole).toDouble() |
|---|
| 351 | |
|---|
| 352 | if ok: # in case we get "?" it is not ok |
|---|
| 353 | if self.widget.showDistributions: |
|---|
| 354 | col = index.column() |
|---|
| 355 | if col < len(self.table.dist) and self.table.dist[col]: # meta attributes and discrete attributes don't have a key |
|---|
| 356 | dist = self.table.dist[col] |
|---|
| 357 | smallerWidth = option.rect.width() * (dist.max - value) / (dist.max-dist.min or 1) |
|---|
| 358 | painter.fillRect(option.rect.adjusted(0,0,-smallerWidth,0), self.widget.distColor) |
|---|
| 359 | ## text = self.widget.locale.toString(value) # we need this to convert doubles like 1.39999999909909 into 1.4 |
|---|
| 360 | ## else: |
|---|
| 361 | text = index.data(Qt.DisplayRole).toString() |
|---|
| 362 | ##text = index.data(OrangeValueRole).toString() |
|---|
| 363 | |
|---|
| 364 | self.drawDisplay(painter, option, option.rect, text) |
|---|
| 365 | painter.restore() |
|---|
| 366 | |
|---|
| 367 | |
|---|
| 368 | |
|---|
| 369 | |
|---|
| 370 | if __name__=="__main__": |
|---|
| 371 | a = QApplication(sys.argv) |
|---|
| 372 | ow = OWDataTable() |
|---|
| 373 | |
|---|
| 374 | #d1 = orange.ExampleTable(r'..\..\doc\datasets\auto-mpg') |
|---|
| 375 | #d2 = orange.ExampleTable('test-labels') |
|---|
| 376 | #d3 = orange.ExampleTable(r'..\..\doc\datasets\sponge.tab') |
|---|
| 377 | #d4 = orange.ExampleTable(r'..\..\doc\datasets\wpbc.csv') |
|---|
| 378 | #d5 = orange.ExampleTable(r'..\..\doc\datasets\adult_sample.tab') |
|---|
| 379 | d5 = orange.ExampleTable(r"E:\Development\Orange Datasets\UCI\wine.tab") |
|---|
| 380 | #d5 = orange.ExampleTable(r"e:\Development\Orange Datasets\Cancer\SRBCT.tab") |
|---|
| 381 | ow.show() |
|---|
| 382 | #ow.dataset(d1,"auto-mpg") |
|---|
| 383 | #ow.dataset(d2,"voting") |
|---|
| 384 | #ow.dataset(d4,"wpbc") |
|---|
| 385 | ow.dataset(d5,"adult_sample") |
|---|
| 386 | a.exec_() |
|---|
| 387 | ow.saveSettings() |
|---|