Skip to content

Instantly share code, notes, and snippets.

@jsexauer
Last active March 28, 2023 21:07
Show Gist options
  • Star 34 You must be signed in to star a gist
  • Fork 12 You must be signed in to fork a gist
  • Save jsexauer/f2bb0cc876828b54f2ed to your computer and use it in GitHub Desktop.
Save jsexauer/f2bb0cc876828b54f2ed to your computer and use it in GitHub Desktop.
A sophisticated GUI to interact with DataFrame objects
"""
DataFrameTable
==============
Quick and Dirty Qt app to view pandas DataFrames. Includes sorting and
filterting.
Based on qtpandas in pandas sandbox module, by Jev Kuznetsov
Usage:
- To quickly display a dataframe, just use DataFrameApp(df)
>>> import sys, pandas
>>> from DataFrameGUI import DataFrameApp
>>> df = pandas.DataFrame([1,2,3])
>>> root = QtGui.QApplication(sys.argv)
>>> app = DataFrameApp(df)
>>> app.show()
>>> root.exec_()
- To build your own widget, subclass DataFrameWidget
"""
import sip
sip.setapi('QString', 2)
sip.setapi('QVariant', 2)
import pandas
import operator
from PyQt4 import QtGui, QtCore
import sys
from functools import partial
class WidgetedCell(object):
"""Set as the value of an element in a pandas DataFrame to create a widget
NOTE: You may also want your widget to implement the getWidgetedCellState and setWidgetedCellState
methods so that interactions with the controlls persist.
"""
def __init__(self, widget):
"""Create a widget in the DataFrameWidget's cell
Args:
widget (subclass of QWidget)
Widget to display in cell. The constructor of `widget` must
accept only one argument, the parent widget to
build `widget` inside of
"""
self.widget = widget
def __repr__(self):
return repr(self.widget)
class DataFrameModel(QtCore.QAbstractTableModel):
""" data model for a DataFrame class """
RawDataRole = 64 # Custom Role, http://qt-project.org/doc/qt-4.8/qt.html#ItemDataRole-enum
RawIndexRole = 65
def __init__(self):
super(DataFrameModel, self).__init__()
self._df = pandas.DataFrame()
self._orig_df = pandas.DataFrame()
self._pre_dyn_filter_df = None
self._resort = lambda : None # Null resort functon
def setDataFrame(self, dataFrame):
"""Set or change pandas DataFrame to show"""
self.df = dataFrame
self._orig_df = dataFrame.copy()
self._pre_dyn_filter_df = None # Clear dynamic filter
@property
def df(self):
return self._df
@df.setter
def df(self, dataFrame):
"""Setter should only be used internal to DataFrameModel. Others should use setDataFrame()"""
self.modelAboutToBeReset.emit()
self._df = dataFrame
self.modelReset.emit()
@QtCore.pyqtSlot()
def beginDynamicFilter(self):
"""Effects of using the "filter" function will not become permanent until endDynamicFilter called"""
if self._pre_dyn_filter_df is None:
print "NEW DYNAMIC FILTER MODEL"
self._pre_dyn_filter_df = self.df.copy()
else:
# Already dynamically filtering, so don't override that
print "SAME DYNAMIC FILTER MODEL"
pass
@QtCore.pyqtSlot()
def endDynamicFilter(self):
"""Makes permanent the effects of the dynamic filter"""
print " * * * RESETING DYNAMIC"
self._pre_dyn_filter_df = None
@QtCore.pyqtSlot()
def cancelDynamicFilter(self):
"""Cancel the dynamic filter"""
self.df = self._pre_dyn_filter_df.copy()
self._pre_dyn_filter_df = None
#------------- table display functions -----------------
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role != QtCore.Qt.DisplayRole:
return None
if orientation == QtCore.Qt.Horizontal:
try:
return '%s' % self.df.columns.tolist()[section]
except (IndexError, ):
return QtCore.QVariant()
elif orientation == QtCore.Qt.Vertical:
try:
return '%s' % self.df.index.tolist()[section]
except (IndexError, ):
return QtCore.QVariant()
def data(self, index, role=QtCore.Qt.DisplayRole):
#if role == QtCore.Qt.BackgroundRole:
# return QtGui.QColor(255,255,204)
if role in (QtCore.Qt.DisplayRole, DataFrameModel.RawDataRole, DataFrameModel.RawIndexRole):
if not index.isValid():
return QtCore.QVariant()
if role == DataFrameModel.RawIndexRole:
r = self.df.index[index.row()]
c = self.df.columns[index.column()]
return (r, c)
data = self.df.iloc[index.row(), index.column()]
if role == DataFrameModel.RawDataRole:
return data
if pandas.isnull(data):
return QtCore.QVariant()
return '%s' % data
else:
return None
def flags(self, index):
defaults = super(DataFrameModel, self).flags(index)
data = self.data(index, DataFrameModel.RawDataRole)
if isinstance(data, WidgetedCell):
return defaults | QtCore.Qt.ItemIsEditable
return defaults
def __setData(self, index, value, role):
row = self.df.index[index.row()]
col = self.df.columns[index.column()]
if hasattr(value, 'toPyObject'):
# PyQt4 gets a QVariant
value = value.toPyObject()
else:
# PySide gets an unicode
dtype = self.df[col].dtype
if dtype != object:
value = None if value == '' else dtype.type(value)
self.df.set_value(row, col, value)
self.dataChanged.emit()
return True
def rowCount(self, index=QtCore.QModelIndex()):
return self.df.shape[0]
def columnCount(self, index=QtCore.QModelIndex()):
return self.df.shape[1]
def sort(self, col_ix, order = QtCore.Qt.AscendingOrder):
if col_ix >= self.df.shape[1]:
# Column out of bounds
return
self.layoutAboutToBeChanged.emit()
ascending = True if order == QtCore.Qt.AscendingOrder else False
self.df = self.df.sort(self.df.columns[col_ix], ascending=ascending)
self.layoutChanged.emit()
# Set sorter to current sort (for future filtering)
self._resort = partial(self.sort, col_ix, order)
def filter(self, col_ix, needle):
"""Filter DataFrame view. Case Insenstive.
Fitlers the DataFrame view to include only rows who's value in col
contains the needle. EX: a needle of "Ab" will show rows with
"absolute" and "REABSOLVE".
Args:
col_ix (int)
Column index in df to filter
needle (str)
String to search df_view for
"""
if self._pre_dyn_filter_df is not None:
df = self._pre_dyn_filter_df.copy()
else:
df = self.df
col = df.columns[col_ix]
# Create lowercase string version of column as series
s_lower = df[col].astype('str').str.lower()
# Make needle lower case too
needle = str(needle).lower()
# Actually filter
self.df = df[s_lower.str.contains(str(needle))]
# Resort
self._resort()
def filterIsIn(self, col_ix, include):
df = self._orig_df
col = self.df.columns[col_ix]
# Convert to string
s_col = df[col].astype('str')
# Filter
self.df = df[s_col.isin(include)]
# Resort
self._resort()
def filterFunction(self, col_ix, function):
df = self.df
col = self.df.columns[col_ix]
self.df = df[function(df[col])]
# Resort
self._resort()
def reset(self):
self.df = self._orig_df.copy()
self._resort = lambda: None
self._pre_dyn_filter_df = None
class DataFrameSortFilterProxyModel(QtGui.QSortFilterProxyModel):
def __init__(self):
super(DataFrameSortFilterProxyModel, self).__init__()
self._accepted_rows = []
self._source_df = None
self._refilter = lambda: None
def setSourceModel(self, source_model):
super(DataFrameSortFilterProxyModel, self).setSourceModel(source_model)
source_model.modelReset.connect(self._source_model_changed)
self._source_model_changed()
def sort(self, *args):
# Delegate sorting to the underyling model
self.sourceModel().sort(*args)
def _source_model_changed(self):
self._source_df = self.sourceModel().df
# Accept all rows
self._accepted_rows = xrange(0, self._source_df.shape[0])
print "SOURCE MODEL CHANGED", len(self._accepted_rows)
if len(self._accepted_rows) > 0:
self.setFilterString('') # Reset the filter
self._refilter()
def setFilterString(self, needle):
"""Filter DataFrame using df[col].str.contains(needle). Case insensitive."""
df = self._source_df
col = df.columns[self.filterKeyColumn()]
# Create lowercase string version of column as series
s_lower = df[col].astype('str').str.lower()
# Make needle lower case too
needle = str(needle).lower()
mask = s_lower.str.contains(str(needle))
self._filter_using_mask(mask)
self._refilter = partial(self.setFilterString, needle)
def setFilterList(self, filter_list):
"""Filter DataFrame using df[col].isin(filter_list)."""
df = self._source_df
col = df.columns[self.filterKeyColumn()]
mask = df[col].isin(filter_list)
self._filter_using_mask(mask)
def setFilterFunction(self, func):
"""Filter DataFrame using df[col].apply(func). Func should return True or False"""
df = self._source_df
col = df.columns[self.filterKeyColumn()]
mask = df[col].apply(func)
self._filter_using_mask(mask)
def _filter_using_mask(self, mask):
# Actually filter (need *locations* of filtered values)
df = self._source_df
col = df.columns[self.filterKeyColumn()]
ilocs = pandas.DataFrame(range(len(df)))
ilocs = ilocs[mask.reset_index(drop=True)]
self.modelAboutToBeReset.emit()
self._accepted_rows = ilocs.index
self.modelReset.emit()
@property
def df(self):
return self._source_df.iloc[self._accepted_rows]
@df.setter
def df(self, val):
raise AttributeError("Tried to set the dataframe of DataFrameSortFilterProxyModel")
def filterAcceptsRow(self, row, idx):
return row in self._accepted_rows
def filterAcceptsColumn(self, col, idx):
# Columns are hidden manually. No need for this
return True
def setFilterRegExp(self, *args):
raise NotImplementedError("Use setFilterString, setFilterList, or setFilterFunc instead")
def setFilterWildcard(self, *args):
raise NotImplementedError("Use setFilterString, setFilterList, or setFilterFunc instead")
def setFilterFixedString(self, *args):
raise NotImplementedError("Use setFilterString, setFilterList, or setFilterFunc instead")
class DynamicFilterLineEdit(QtGui.QLineEdit):
"""Filter textbox for a DataFrameTable"""
def __init__(self, *args, **kwargs):
self._always_dynamic = kwargs.pop('always_dynamic', False)
super(DynamicFilterLineEdit, self).__init__(*args, **kwargs)
self.col_to_filter = None
self._orig_df = None
self._host = None
def bind_dataframewidget(self, host, col_ix):
"""Bind tihs DynamicFilterLineEdit to a DataFrameTable's column
Args:
host (DataFrameWidget)
Host to filter
col_ix (int)
Index of column of host to filter
"""
self.host = host
self.col_to_filter = col_ix
self.textChanged.connect(self._update_filter)
@property
def host(self):
if self._host is None:
raise RuntimeError("Must call bind_dataframewidget() "
"before use.")
else:
return self._host
@host.setter
def host(self, value):
if not isinstance(value, DataFrameWidget):
raise ValueError("Must bind to a DataFrameWidget, not %s" % value)
else:
self._host = value
if not self._always_dynamic:
self.editingFinished.connect(self._host._data_model.endDynamicFilter)
def focusInEvent(self, QFocusEvent):
self._host._data_model.beginDynamicFilter()
def _update_filter(self, text):
"""Called everytime we type in the filter box"""
col_ix = self.col_to_filter
self.host.filter(col_ix, text)
class DynamicFilterMenuAction(QtGui.QWidgetAction):
"""Filter textbox in column-header right-click menu"""
def __init__(self, parent, menu, col_ix):
"""Filter textbox in column right-click menu
Args:
parent (DataFrameWidget)
Parent who owns the DataFrame to filter
menu (QMenu)
Menu object I am located on
col_ix (int)
Index of column used in pandas DataFrame we are to filter
"""
super(DynamicFilterMenuAction, self).__init__(parent)
# State
self.parent_menu = menu
# Build Widgets
widget = QtGui.QWidget()
layout = QtGui.QHBoxLayout()
self.label = QtGui.QLabel('Filter')
self.text_box = DynamicFilterLineEdit()
self.text_box.bind_dataframewidget(self.parent(), col_ix)
self.text_box.returnPressed.connect(self._close_menu)
layout.addWidget(self.label)
layout.addWidget(self.text_box)
widget.setLayout(layout)
self.setDefaultWidget(widget)
def _close_menu(self):
"""Gracefully handle menu"""
self.parent_menu.close()
class FilterListMenuWidget(QtGui.QWidgetAction):
"""Filter textbox in column-right click menu"""
def __init__(self, parent, menu, col_ix):
"""Filter textbox in column right-click menu
Args:
parent (DataFrameWidget)
Parent who owns the DataFrame to filter
menu (QMenu)
Menu object I am located on
col_ix (int)
Column index used in pandas DataFrame we are to filter
label (str)
Label in popup menu
"""
super(FilterListMenuWidget, self).__init__(parent)
# State
self.menu = menu
self.col_ix = col_ix
# Build Widgets
widget = QtGui.QWidget()
layout = QtGui.QVBoxLayout()
self.list = QtGui.QListWidget()
self.list.setFixedHeight(100)
layout.addWidget(self.list)
widget.setLayout(layout)
self.setDefaultWidget(widget)
# Signals/slots
self.list.itemChanged.connect(self.on_list_itemChanged)
self.parent().dataFrameChanged.connect(self._populate_list)
self._populate_list(inital=True)
def _populate_list(self, inital=False):
self.list.clear()
df = self.parent()._data_model._orig_df
col = df.columns[self.col_ix]
full_col = set(df[col]) # All Entries possible in this column
disp_col = set(self.parent().df[col]) # Entries currently displayed
def _build_item(item, state=None):
i = QtGui.QListWidgetItem('%s' % item)
i.setFlags(i.flags() | QtCore.Qt.ItemIsUserCheckable)
if state is None:
if item in disp_col:
state = QtCore.Qt.Checked
else:
state = QtCore.Qt.Unchecked
i.setCheckState(state)
i.checkState()
self.list.addItem(i)
return i
# Add a (Select All)
if full_col == disp_col:
select_all_state = QtCore.Qt.Checked
else:
select_all_state = QtCore.Qt.Unchecked
self._action_select_all = _build_item('(Select All)', state=select_all_state)
# Add filter items
if inital:
build_list = full_col
else:
build_list = disp_col
for i in sorted(build_list):
_build_item(i)
# Add a (Blanks)
# TODO
def on_list_itemChanged(self, item):
###
# Figure out what "select all" check-box state should be
###
self.list.blockSignals(True)
if item is self._action_select_all:
# Handle "select all" item click
if item.checkState() == QtCore.Qt.Checked:
state = QtCore.Qt.Checked
else:
state = QtCore.Qt.Unchecked
# Select/deselect all items
for i in range(self.list.count()):
if i is self._action_select_all: continue
i = self.list.item(i)
i.setCheckState(state)
else:
# Non "select all" item; figure out what "select all" should be
if item.checkState() == QtCore.Qt.Unchecked:
self._action_select_all.setCheckState(QtCore.Qt.Unchecked)
else:
# "select all" only checked if all other items are checked
for i in range(self.list.count()):
i = self.list.item(i)
if i is self._action_select_all: continue
if i.checkState() == QtCore.Qt.Unchecked:
self._action_select_all.setCheckState(QtCore.Qt.Unchecked)
break
else:
self._action_select_all.setCheckState(QtCore.Qt.Checked)
self.list.blockSignals(False)
###
# Filter dataframe according to list
###
include = []
for i in range(self.list.count()):
i = self.list.item(i)
if i is self._action_select_all: continue
if i.checkState() == QtCore.Qt.Checked:
include.append(str(i.text()))
self.parent().blockSignals(True)
self.parent().filterIsIn(self.col_ix, include)
self.parent().blockSignals(False)
self.parent()._enable_widgeted_cells()
class DataFrameItemDelegate(QtGui.QStyledItemDelegate):
"""Implements WidgetedCell"""
def __init__(self):
super(DataFrameItemDelegate, self).__init__()
self._cell_widget_states = {}
def createEditor(self, parent, option, index):
data = index.data(DataFrameModel.RawDataRole)
true_index = index.data(DataFrameModel.RawIndexRole)
if isinstance(data, WidgetedCell):
# Make new widget
widget_class = data.widget
# Give out widget our cell parent so it knows where to paint
widget = widget_class(parent)
try:
# Find existing widget if we can
widget_state = self._cell_widget_states[true_index]
except KeyError:
pass
else:
try:
widget.setWidgetedCellState(widget_state)
except AttributeError:
# Not implementing the WidgetedCell interface
pass
return widget
else:
return super(DataFrameItemDelegate, self).createEditor(parent, option, index)
def setModelData(self, widget, model, index):
# Try to save the state of the widget
try:
widget_state = widget.getWidgetedCellState()
except AttributeError:
# Not implementing the WidgetedCell interface
return
true_index = index.data(DataFrameModel.RawIndexRole)
self._cell_widget_states[true_index] = widget_state
def paint(self, painter, option, index):
d = index.data(DataFrameModel.RawDataRole)
if isinstance(d, WidgetedCell):
# Don't paint, create editor instead
return None
else:
return super(DataFrameItemDelegate, self).paint(painter, option, index)
class DataFrameWidget(QtGui.QTableView):
dataFrameChanged = QtCore.pyqtSignal()
cellClicked = QtCore.pyqtSignal(int, int)
def __init__(self, parent=None, df=None):
"""DataFrameTable
Create a widget to display a pandas DataFrame.
Args:
parent (QObject)
Parent object (likely window or canvas)
df (pandas DataFrame, optional)
DataFrame to display
"""
super(DataFrameWidget, self).__init__(parent)
self.defaultExcelFile = "temp.xls"
self.defaultExcelSheet = "Output"
# Set up view
self._data_model = DataFrameModel()
self.setModel(self._data_model)
# Signals/Slots
self._data_model.modelReset.connect(self.dataFrameChanged)
self._data_model.dataChanged.connect(self.dataFrameChanged)
self.clicked.connect(self._on_click)
self.dataFrameChanged.connect(self._enable_widgeted_cells)
# Set up delegate Delegate
delegate = DataFrameItemDelegate()
self.setItemDelegate(delegate)
# Show the edit widget as soon as the user clicks in the cell
# (needed for item delegate)
self.setEditTriggers(self.CurrentChanged)
# Initilize to passed dataframe
if df is None:
df = pandas.DataFrame()
self._data_model.setDataFrame(df)
#self.setSortingEnabled(True)
# Create header menu bindings
self.horizontalHeader().setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.horizontalHeader().customContextMenuRequested.connect(self._header_menu)
self._enable_widgeted_cells()
def make_cell_context_menu(self, menu, row_ix, col_ix):
"""Create the mneu displayed when right-clicking on a cell.
Overrite this method to add custom right-click options
Args:
menu (QMenu)
Menu to which to add actions
row_ix (int)
Row location in dataframe
col_ix (int)
Coloumn location in dataframe
Returns:
menu (QMenu)
Same menu passed in, with added actions
"""
cell_val = self.df.iget_value(row_ix, col_ix)
# Quick Filter
def _quick_filter(s_col):
return s_col == cell_val
menu.addAction(self._icon('CommandLink'),
"Quick Filter", partial(self._data_model.filterFunction, col_ix=col_ix, function=_quick_filter))
# GreaterThan/LessThan filter
def _cmp_filter(s_col, op):
return op(s_col, cell_val)
menu.addAction("Show Greater Than",
partial(self._data_model.filterFunction, col_ix=col_ix,
function=partial(_cmp_filter, op=operator.ge)))
menu.addAction("Show Less Than",
partial(self._data_model.filterFunction, col_ix=col_ix,
function=partial(_cmp_filter, op=operator.le)))
menu.addAction(self._icon('DialogResetButton'),
"Clear",
self._data_model.reset)
menu.addSeparator()
# Save to Excel
def _to_excel():
from subprocess import Popen
self.df.to_excel(self.defaultExcelFile, self.defaultExcelSheet)
Popen(self.defaultExcelFile, shell=True)
menu.addAction("Open in Excel",
_to_excel)
return menu
def contextMenuEvent(self, event):
"""Implements right-clicking on cell.
NOTE: You probably want to overrite make_cell_context_menu, not this
function, when subclassing.
"""
row_ix = self.rowAt(event.y())
col_ix = self.columnAt(event.x())
if row_ix < 0 or col_ix < 0:
return #out of bounds
menu = QtGui.QMenu(self)
menu = self.make_cell_context_menu(menu, row_ix, col_ix)
menu.exec_(self.mapToGlobal(event.pos()))
def _header_menu(self, pos):
"""Create popup menu used for header"""
menu = QtGui.QMenu(self)
col_ix = self.horizontalHeader().logicalIndexAt(pos)
if col_ix == -1:
# Out of bounds
return
# Filter Menu Action
menu.addAction(DynamicFilterMenuAction(self, menu, col_ix))
menu.addAction(FilterListMenuWidget(self, menu, col_ix))
menu.addAction(self._icon('DialogResetButton'),
"Reset",
self._data_model.reset)
# Sort Ascending/Decending Menu Action
menu.addAction(self._icon('TitleBarShadeButton'),
"Sort Ascending",
partial(self._data_model.sort, col_ix=col_ix, order=QtCore.Qt.AscendingOrder))
menu.addAction(self._icon('TitleBarUnshadeButton'),
"Sort Descending",
partial(self._data_model.sort, col_ix=col_ix, order=QtCore.Qt.DescendingOrder))
menu.addSeparator()
# Hide
menu.addAction("Hide", partial(self.hideColumn, col_ix))
# Show (column to left and right)
for i in (-1, 1):
if self.isColumnHidden(col_ix+i):
menu.addAction("Show %s" % self._data_model.headerData(col_ix+i, QtCore.Qt.Horizontal),
partial(self.showColumn, col_ix+i))
menu.exec_(self.mapToGlobal(pos))
def setDataFrame(self, df):
self._data_model.setDataFrame(df)
self.resizeColumnsToContents()
def filter(self, col_ix, needle):
return self._data_model.filter(col_ix, needle)
def filterIsIn(self, col_ix, include):
return self._data_model.filterIsIn(col_ix, include)
@property
def df(self):
return self._data_model.df
@df.setter
def df(self, dataFrame):
# Use the "hard setting" of the dataframe because anyone who's interacting with the
# DataFrameWidget (ie, end user) would be setting this
self._data_model.setDataFrame(dataFrame)
def keyPressEvent(self, event):
"""Implements keyboard shortcuts"""
if event.matches(QtGui.QKeySequence.Copy):
self.copy()
else:
# Pass up
super(DataFrameWidget, self).keyPressEvent(event)
def copy(self):
"""Copy selected cells into copy-buffer"""
selection = self.selectionModel()
indexes = selection.selectedIndexes()
if len(indexes) < 1:
# Nothing selected
return
# Capture selection into a DataFrame
items = pandas.DataFrame()
for idx in indexes:
row = idx.row()
col = idx.column()
item = idx.data()
if item:
items = items.set_value(row, col, str(item.toString()))
# Make into tab-delimited text (best for Excel)
items = list(items.itertuples(index=False))
s = '\n'.join(['\t'.join([cell for cell in row]) for row in items])
# Send to clipboard
QtGui.QApplication.clipboard().setText(s)
def _icon(self, icon_name):
"""Convinence function to get standard icons from Qt"""
if not icon_name.startswith('SP_'):
icon_name = 'SP_' + icon_name
icon = getattr(QtGui.QStyle, icon_name, None)
if icon is None:
raise Exception("Unknown icon %s" % icon_name)
return self.style().standardIcon(icon)
def _on_click(self, index):
if index.isValid():
self.cellClicked.emit(index.row(), index.column())
def _enable_widgeted_cells(self):
# Update all cells with WidgetedCell to have persistent editors
model = self.model()
if model is None:
return
for r in xrange(model.rowCount()):
for c in xrange(model.columnCount()):
idx = model.index(r,c)
d = model.data(idx, DataFrameModel.RawDataRole)
if isinstance(d, WidgetedCell):
self.openPersistentEditor(idx)
class DataFrameApp(QtGui.QMainWindow):
"""Sample DataFrameTable Application"""
def __init__(self, df, title="Inspecting DataFrame"):
super(DataFrameApp, self).__init__()
# State variables
self.title_base = title
# Initialize main data table
self.table = DataFrameWidget(self)
self.table.dataFrameChanged.connect(self.datatable_updated)
self.table.setDataFrame(df)
self.setCentralWidget(self.table)
# Set window size
col_size = sum([self.table.columnWidth(i) for i in range(0,99)])
col_size = min(col_size+75, 1500)
self.setGeometry(300, 300, col_size, 250)
def datatable_updated(self):
# Change title to reflect updated size
df = self.table.df
title = self.title_base + ' [%dx%d]' % (len(df.index), len(df.columns))
self.setWindowTitle(title)
class ExampleWidgetForWidgetedCell(QtGui.QComboBox):
"""
To implement a persistent state for the widgetd cell, you must provide
a `getWidgetedCellState` and `setWidgetedCellState` methods. This is how
the WidgetedCell framework can create and destory your widget as needed.
"""
def __init__(self, parent):
super(ExampleWidgetForWidgetedCell, self).__init__(parent)
self.addItem("Option A")
self.addItem("Option B")
self.addItem("Option C")
self.setCurrentIndex(0)
def getWidgetedCellState(self):
return self.currentIndex()
def setWidgetedCellState(self, state):
self.setCurrentIndex(state)
if __name__ == '__main__':
# Create a quick example
_app = QtGui.QApplication(sys.argv)
import string
import random
rnd_txt = lambda: "".join( [random.choice(string.letters[:26]) for i in xrange(15)] )
df = [['a','b','c']*3]
for j in xrange(5):
r = []
for k in xrange(6):
r.append(rnd_txt())
r.append(random.randint(1,20))
r.append(random.random()*10)
r.append(WidgetedCell(ExampleWidgetForWidgetedCell))
df.append(r)
df = pandas.DataFrame(df, columns=['AAA','BBB','CCC','DDD','EEE','FFF','GGG','HHH','III'])
app = DataFrameApp(df)
app.show()
_app.exec_()
@JmiXIII
Copy link

JmiXIII commented Jul 28, 2015

Hi,

I am not a python programmer, but I needed to use your code in python3.
I just change print + xrange + letters => it seems fisrtly Ok
Yet running it in notebook ends up with when I close the qt frame in a kernel error.
Is it normal ?

Do you think it could possible to implement a live dataframe modification ?
Anyway thanks for your code.

"""
DataFrameTable
==============
Quick and Dirty Qt app to view pandas DataFrames.  Includes sorting and
filterting.
Based on qtpandas in pandas sandbox module, by Jev Kuznetsov
Usage:
 - To quickly display a dataframe, just use DataFrameApp(df)
 >>> import sys, pandas
 >>> from DataFrameGUI import DataFrameApp
 >>> df = pandas.DataFrame([1,2,3])
 >>> root = QtGui.QApplication(sys.argv)
 >>> app = DataFrameApp(df)
 >>> app.show()
 >>> root.exec_()
 - To build your own widget, subclass DataFrameWidget
"""

import sip
sip.setapi('QString', 2)
sip.setapi('QVariant', 2)


import pandas
import operator
from PyQt4 import QtGui, QtCore
import sys

from functools import partial

class WidgetedCell(object):
    """Set as the value of an element in a pandas DataFrame to create a widget
    NOTE: You may also want your widget to implement the getWidgetedCellState and setWidgetedCellState
    methods so that interactions with the controlls persist.
    """
    def __init__(self, widget):
        """Create a widget in the DataFrameWidget's cell
        Args:
            widget (subclass of QWidget)
                Widget to display in cell.  The constructor of `widget` must
                accept only one argument, the parent widget to
                build `widget` inside of
        """
        self.widget = widget

    def __repr__(self):
        return repr(self.widget)

class DataFrameModel(QtCore.QAbstractTableModel):
    """ data model for a DataFrame class """

    RawDataRole = 64    # Custom Role, http://qt-project.org/doc/qt-4.8/qt.html#ItemDataRole-enum
    RawIndexRole = 65


    def __init__(self):
        super(DataFrameModel, self).__init__()
        self._df = pandas.DataFrame()
        self._orig_df = pandas.DataFrame()
        self._pre_dyn_filter_df = None
        self._resort = lambda : None # Null resort functon


    def setDataFrame(self, dataFrame):
        """Set or change pandas DataFrame to show"""
        self.df = dataFrame
        self._orig_df = dataFrame.copy()
        self._pre_dyn_filter_df = None # Clear dynamic filter

    @property
    def df(self):
        return self._df
    @df.setter
    def df(self, dataFrame):
        """Setter should only be used internal to DataFrameModel.  Others should use setDataFrame()"""
        self.modelAboutToBeReset.emit()
        self._df = dataFrame
        self.modelReset.emit()

    @QtCore.pyqtSlot()
    def beginDynamicFilter(self):
        """Effects of using the "filter" function will not become permanent until endDynamicFilter called"""
        if self._pre_dyn_filter_df is None:
            print ("NEW DYNAMIC FILTER MODEL")
            self._pre_dyn_filter_df = self.df.copy()
        else:
            # Already dynamically filtering, so don't override that
            print ("SAME DYNAMIC FILTER MODEL")
            pass

    @QtCore.pyqtSlot()
    def endDynamicFilter(self):
        """Makes permanent the effects of the dynamic filter"""
        print (" * * * RESETING DYNAMIC")
        self._pre_dyn_filter_df = None

    @QtCore.pyqtSlot()
    def cancelDynamicFilter(self):
        """Cancel the dynamic filter"""
        self.df = self._pre_dyn_filter_df.copy()
        self._pre_dyn_filter_df = None


    #------------- table display functions -----------------
    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
        if role != QtCore.Qt.DisplayRole:
            return None

        if orientation == QtCore.Qt.Horizontal:
            try:
                return '%s' % self.df.columns.tolist()[section]
            except (IndexError, ):
                return QtCore.QVariant()
        elif orientation == QtCore.Qt.Vertical:
            try:
                return '%s' % self.df.index.tolist()[section]
            except (IndexError, ):
                return QtCore.QVariant()

    def data(self, index, role=QtCore.Qt.DisplayRole):
        #if role == QtCore.Qt.BackgroundRole:
        #    return QtGui.QColor(255,255,204)
        if role in (QtCore.Qt.DisplayRole, DataFrameModel.RawDataRole, DataFrameModel.RawIndexRole):
            if not index.isValid():
                return QtCore.QVariant()

            if role == DataFrameModel.RawIndexRole:
                r = self.df.index[index.row()]
                c = self.df.columns[index.column()]
                return (r, c)
            data = self.df.iloc[index.row(), index.column()]


            if role == DataFrameModel.RawDataRole:
                return data

            if pandas.isnull(data):
                return QtCore.QVariant()
            return '%s' % data
        else:
            return None

    def flags(self, index):
        defaults = super(DataFrameModel, self).flags(index)
        data = self.data(index, DataFrameModel.RawDataRole)
        if isinstance(data, WidgetedCell):
            return defaults | QtCore.Qt.ItemIsEditable
        return defaults

    def __setData(self, index, value, role):
        row = self.df.index[index.row()]
        col = self.df.columns[index.column()]
        if hasattr(value, 'toPyObject'):
            # PyQt4 gets a QVariant
            value = value.toPyObject()
        else:
            # PySide gets an unicode
            dtype = self.df[col].dtype
            if dtype != object:
                value = None if value == '' else dtype.type(value)
        self.df.set_value(row, col, value)
        self.dataChanged.emit()
        return True

    def rowCount(self, index=QtCore.QModelIndex()):
        return self.df.shape[0]

    def columnCount(self, index=QtCore.QModelIndex()):
        return self.df.shape[1]

    def sort(self, col_ix, order = QtCore.Qt.AscendingOrder):
        if col_ix >= self.df.shape[1]:
            # Column out of bounds
            return

        self.layoutAboutToBeChanged.emit()
        ascending = True if order == QtCore.Qt.AscendingOrder else False
        self.df = self.df.sort(self.df.columns[col_ix], ascending=ascending)
        self.layoutChanged.emit()

        # Set sorter to current sort (for future filtering)
        self._resort = partial(self.sort, col_ix, order)

    def filter(self, col_ix, needle):
        """Filter DataFrame view.  Case Insenstive.
        Fitlers the DataFrame view to include only rows who's value in col
        contains the needle. EX: a needle of "Ab" will show rows with
        "absolute" and "REABSOLVE".
        Args:
            col_ix (int)
                Column index in df to filter
            needle (str)
                String to search df_view for
        """

        if self._pre_dyn_filter_df is not None:
            df = self._pre_dyn_filter_df.copy()
        else:
            df = self.df
        col = df.columns[col_ix]
        # Create lowercase string version of column as series
        s_lower = df[col].astype('str').str.lower()

        # Make needle lower case too
        needle = str(needle).lower()

        # Actually filter
        self.df = df[s_lower.str.contains(str(needle))]

        # Resort
        self._resort()

    def filterIsIn(self, col_ix, include):
        df = self._orig_df
        col = self.df.columns[col_ix]

        # Convert to string
        s_col = df[col].astype('str')

        # Filter
        self.df = df[s_col.isin(include)]

        # Resort
        self._resort()

    def filterFunction(self, col_ix, function):
        df = self.df
        col = self.df.columns[col_ix]

        self.df = df[function(df[col])]

        # Resort
        self._resort()

    def reset(self):
        self.df = self._orig_df.copy()
        self._resort = lambda: None
        self._pre_dyn_filter_df = None


class DataFrameSortFilterProxyModel(QtGui.QSortFilterProxyModel):
    def __init__(self):
        super(DataFrameSortFilterProxyModel, self).__init__()

        self._accepted_rows = []
        self._source_df = None
        self._refilter = lambda: None

    def setSourceModel(self, source_model):
        super(DataFrameSortFilterProxyModel, self).setSourceModel(source_model)

        source_model.modelReset.connect(self._source_model_changed)
        self._source_model_changed()

    def sort(self, *args):
        # Delegate sorting to the underyling model
        self.sourceModel().sort(*args)

    def _source_model_changed(self):
        self._source_df = self.sourceModel().df
        # Accept all rows
        self._accepted_rows = range(0, self._source_df.shape[0])
        print ("SOURCE MODEL CHANGED", len(self._accepted_rows))
        if len(self._accepted_rows) > 0:
            self.setFilterString('')    # Reset the filter
        self._refilter()

    def setFilterString(self, needle):
        """Filter DataFrame using df[col].str.contains(needle).  Case insensitive."""
        df = self._source_df
        col = df.columns[self.filterKeyColumn()]

        # Create lowercase string version of column as series
        s_lower = df[col].astype('str').str.lower()

        # Make needle lower case too
        needle = str(needle).lower()

        mask = s_lower.str.contains(str(needle))
        self._filter_using_mask(mask)
        self._refilter = partial(self.setFilterString, needle)

    def setFilterList(self, filter_list):
        """Filter DataFrame using df[col].isin(filter_list)."""
        df = self._source_df
        col = df.columns[self.filterKeyColumn()]

        mask = df[col].isin(filter_list)
        self._filter_using_mask(mask)

    def setFilterFunction(self, func):
        """Filter DataFrame using df[col].apply(func).  Func should return True or False"""
        df = self._source_df
        col = df.columns[self.filterKeyColumn()]

        mask = df[col].apply(func)
        self._filter_using_mask(mask)

    def _filter_using_mask(self, mask):
        # Actually filter (need *locations* of filtered values)
        df = self._source_df
        col = df.columns[self.filterKeyColumn()]

        ilocs = pandas.DataFrame(range(len(df)))
        ilocs = ilocs[mask.reset_index(drop=True)]

        self.modelAboutToBeReset.emit()
        self._accepted_rows = ilocs.index
        self.modelReset.emit()


    @property
    def df(self):
        return self._source_df.iloc[self._accepted_rows]
    @df.setter
    def df(self, val):
        raise AttributeError("Tried to set the dataframe of DataFrameSortFilterProxyModel")

    def filterAcceptsRow(self, row, idx):
        return row in self._accepted_rows

    def filterAcceptsColumn(self, col, idx):
        # Columns are hidden manually.  No need for this
        return True

    def setFilterRegExp(self, *args):
        raise NotImplementedError("Use setFilterString, setFilterList, or setFilterFunc instead")

    def setFilterWildcard(self, *args):
        raise NotImplementedError("Use setFilterString, setFilterList, or setFilterFunc instead")

    def setFilterFixedString(self, *args):
        raise NotImplementedError("Use setFilterString, setFilterList, or setFilterFunc instead")


class DynamicFilterLineEdit(QtGui.QLineEdit):
    """Filter textbox for a DataFrameTable"""
    def __init__(self, *args, **kwargs):
        self._always_dynamic = kwargs.pop('always_dynamic', False)

        super(DynamicFilterLineEdit, self).__init__(*args, **kwargs)

        self.col_to_filter = None
        self._orig_df = None
        self._host = None

    def bind_dataframewidget(self, host, col_ix):
        """Bind tihs DynamicFilterLineEdit to a DataFrameTable's column
        Args:
            host (DataFrameWidget)
                Host to filter
            col_ix (int)
                Index of column of host to filter
        """
        self.host = host
        self.col_to_filter = col_ix
        self.textChanged.connect(self._update_filter)

    @property
    def host(self):
        if self._host is None:
            raise RuntimeError("Must call bind_dataframewidget() "
            "before use.")
        else:
            return self._host
    @host.setter
    def host(self, value):
        if not isinstance(value, DataFrameWidget):
            raise ValueError("Must bind to a DataFrameWidget, not %s" % value)
        else:
            self._host = value

        if not self._always_dynamic:
            self.editingFinished.connect(self._host._data_model.endDynamicFilter)

    def focusInEvent(self, QFocusEvent):
        self._host._data_model.beginDynamicFilter()

    def _update_filter(self, text):
        """Called everytime we type in the filter box"""
        col_ix = self.col_to_filter

        self.host.filter(col_ix, text)


class DynamicFilterMenuAction(QtGui.QWidgetAction):
    """Filter textbox in column-header right-click menu"""
    def __init__(self, parent, menu, col_ix):
        """Filter textbox in column right-click menu
        Args:
            parent (DataFrameWidget)
                Parent who owns the DataFrame to filter
            menu (QMenu)
                Menu object I am located on
            col_ix (int)
                Index of column used in pandas DataFrame we are to filter
        """
        super(DynamicFilterMenuAction, self).__init__(parent)

        # State
        self.parent_menu = menu

        # Build Widgets
        widget = QtGui.QWidget()
        layout = QtGui.QHBoxLayout()
        self.label = QtGui.QLabel('Filter')
        self.text_box = DynamicFilterLineEdit()
        self.text_box.bind_dataframewidget(self.parent(), col_ix)
        self.text_box.returnPressed.connect(self._close_menu)

        layout.addWidget(self.label)
        layout.addWidget(self.text_box)
        widget.setLayout(layout)

        self.setDefaultWidget(widget)

    def _close_menu(self):
        """Gracefully handle menu"""
        self.parent_menu.close()


class FilterListMenuWidget(QtGui.QWidgetAction):
    """Filter textbox in column-right click menu"""
    def __init__(self, parent, menu, col_ix):
        """Filter textbox in column right-click menu
        Args:
            parent (DataFrameWidget)
                Parent who owns the DataFrame to filter
            menu (QMenu)
                Menu object I am located on
            col_ix (int)
                Column index used in pandas DataFrame we are to filter
            label (str)
                Label in popup menu
        """
        super(FilterListMenuWidget, self).__init__(parent)

        # State
        self.menu = menu
        self.col_ix = col_ix

        # Build Widgets
        widget = QtGui.QWidget()
        layout = QtGui.QVBoxLayout()
        self.list = QtGui.QListWidget()
        self.list.setFixedHeight(100)

        layout.addWidget(self.list)
        widget.setLayout(layout)

        self.setDefaultWidget(widget)

        # Signals/slots
        self.list.itemChanged.connect(self.on_list_itemChanged)
        self.parent().dataFrameChanged.connect(self._populate_list)

        self._populate_list(inital=True)

    def _populate_list(self, inital=False):
        self.list.clear()

        df = self.parent()._data_model._orig_df
        col = df.columns[self.col_ix]
        full_col = set(df[col])  # All Entries possible in this column
        disp_col = set(self.parent().df[col]) # Entries currently displayed

        def _build_item(item, state=None):
            i = QtGui.QListWidgetItem('%s' % item)
            i.setFlags(i.flags() | QtCore.Qt.ItemIsUserCheckable)
            if state is None:
                if item in disp_col:
                    state = QtCore.Qt.Checked
                else:
                    state = QtCore.Qt.Unchecked
            i.setCheckState(state)
            i.checkState()
            self.list.addItem(i)
            return i

        # Add a (Select All)
        if full_col == disp_col:
            select_all_state = QtCore.Qt.Checked
        else:
            select_all_state = QtCore.Qt.Unchecked
        self._action_select_all = _build_item('(Select All)', state=select_all_state)

        # Add filter items
        if inital:
            build_list = full_col
        else:
            build_list = disp_col
        for i in sorted(build_list):
            _build_item(i)

        # Add a (Blanks)
        # TODO

    def on_list_itemChanged(self, item):
        ###
        # Figure out what "select all" check-box state should be
        ###
        self.list.blockSignals(True)
        if item is self._action_select_all:
            # Handle "select all" item click
            if item.checkState() == QtCore.Qt.Checked:
                state = QtCore.Qt.Checked
            else:
                state = QtCore.Qt.Unchecked
            # Select/deselect all items
            for i in range(self.list.count()):
                if i is self._action_select_all: continue
                i = self.list.item(i)
                i.setCheckState(state)
        else:
            # Non "select all" item; figure out what "select all" should be
            if item.checkState() == QtCore.Qt.Unchecked:
                self._action_select_all.setCheckState(QtCore.Qt.Unchecked)
            else:
                # "select all" only checked if all other items are checked
                for i in range(self.list.count()):
                    i = self.list.item(i)
                    if i is self._action_select_all: continue
                    if i.checkState() == QtCore.Qt.Unchecked:
                        self._action_select_all.setCheckState(QtCore.Qt.Unchecked)
                        break
                else:
                    self._action_select_all.setCheckState(QtCore.Qt.Checked)
        self.list.blockSignals(False)

        ###
        # Filter dataframe according to list
        ###
        include = []
        for i in range(self.list.count()):
            i = self.list.item(i)
            if i is self._action_select_all: continue
            if i.checkState() == QtCore.Qt.Checked:
                include.append(str(i.text()))

        self.parent().blockSignals(True)
        self.parent().filterIsIn(self.col_ix, include)
        self.parent().blockSignals(False)
        self.parent()._enable_widgeted_cells()

class DataFrameItemDelegate(QtGui.QStyledItemDelegate):
    """Implements WidgetedCell"""

    def __init__(self):
        super(DataFrameItemDelegate, self).__init__()
        self._cell_widget_states = {}


    def createEditor(self, parent, option, index):
        data = index.data(DataFrameModel.RawDataRole)
        true_index = index.data(DataFrameModel.RawIndexRole)
        if isinstance(data, WidgetedCell):
            # Make new widget
            widget_class = data.widget
            # Give out widget our cell parent so it knows where to paint
            widget = widget_class(parent)
            try:
                # Find existing widget if we can
                widget_state = self._cell_widget_states[true_index]
            except KeyError:
                pass
            else:
                try:
                    widget.setWidgetedCellState(widget_state)
                except AttributeError:
                    # Not implementing the WidgetedCell interface
                    pass
            return widget
        else:
            return super(DataFrameItemDelegate, self).createEditor(parent, option, index)


    def setModelData(self, widget, model, index):
        # Try to save the state of the widget
        try:
            widget_state = widget.getWidgetedCellState()
        except AttributeError:
            # Not implementing the WidgetedCell interface
            return
        true_index = index.data(DataFrameModel.RawIndexRole)
        self._cell_widget_states[true_index] = widget_state



    def paint(self, painter, option, index):
        d = index.data(DataFrameModel.RawDataRole)
        if isinstance(d, WidgetedCell):
            # Don't paint, create editor instead
            return None
        else:
            return super(DataFrameItemDelegate, self).paint(painter, option, index)


class DataFrameWidget(QtGui.QTableView):

    dataFrameChanged = QtCore.pyqtSignal()
    cellClicked = QtCore.pyqtSignal(int, int)

    def __init__(self, parent=None, df=None):
        """DataFrameTable
        Create a widget to display a pandas DataFrame.
        Args:
            parent (QObject)
                Parent object (likely window or canvas)
            df (pandas DataFrame, optional)
                DataFrame to display
        """
        super(DataFrameWidget, self).__init__(parent)


        self.defaultExcelFile = "temp.xls"
        self.defaultExcelSheet = "Output"

        # Set up view
        self._data_model = DataFrameModel()
        self.setModel(self._data_model)

        # Signals/Slots
        self._data_model.modelReset.connect(self.dataFrameChanged)
        self._data_model.dataChanged.connect(self.dataFrameChanged)
        self.clicked.connect(self._on_click)
        self.dataFrameChanged.connect(self._enable_widgeted_cells)

        # Set up delegate Delegate
        delegate = DataFrameItemDelegate()
        self.setItemDelegate(delegate)
        # Show the edit widget as soon as the user clicks in the cell
        #  (needed for item delegate)
        self.setEditTriggers(self.CurrentChanged)

        # Initilize to passed dataframe
        if df is None:
            df = pandas.DataFrame()
        self._data_model.setDataFrame(df)


        #self.setSortingEnabled(True)

        # Create header menu bindings
        self.horizontalHeader().setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.horizontalHeader().customContextMenuRequested.connect(self._header_menu)

        self._enable_widgeted_cells()

    def make_cell_context_menu(self, menu, row_ix, col_ix):
        """Create the mneu displayed when right-clicking on a cell.
        Overrite this method to add custom right-click options
        Args:
            menu (QMenu)
                Menu to which to add actions
            row_ix (int)
                Row location in dataframe
            col_ix (int)
                Coloumn location in dataframe
        Returns:
            menu (QMenu)
                Same menu passed in, with added actions
        """
        cell_val = self.df.iget_value(row_ix, col_ix)

        # Quick Filter
        def _quick_filter(s_col):
            return s_col == cell_val
        menu.addAction(self._icon('CommandLink'),
            "Quick Filter", partial(self._data_model.filterFunction, col_ix=col_ix, function=_quick_filter))

        # GreaterThan/LessThan filter
        def _cmp_filter(s_col, op):
            return op(s_col, cell_val)
        menu.addAction("Show Greater Than",
                        partial(self._data_model.filterFunction, col_ix=col_ix,
                                function=partial(_cmp_filter, op=operator.ge)))
        menu.addAction("Show Less Than",
                        partial(self._data_model.filterFunction, col_ix=col_ix,
                                function=partial(_cmp_filter, op=operator.le)))
        menu.addAction(self._icon('DialogResetButton'),
                        "Clear",
                        self._data_model.reset)
        menu.addSeparator()

        # Save to Excel
        def _to_excel():
            from subprocess import Popen
            self.df.to_excel(self.defaultExcelFile, self.defaultExcelSheet)
            Popen(self.defaultExcelFile, shell=True)
        menu.addAction("Open in Excel",
                       _to_excel)

        return menu


    def contextMenuEvent(self, event):
        """Implements right-clicking on cell.
        NOTE: You probably want to overrite make_cell_context_menu, not this
        function, when subclassing.
        """
        row_ix = self.rowAt(event.y())
        col_ix = self.columnAt(event.x())

        if row_ix < 0 or col_ix < 0:
            return #out of bounds

        menu = QtGui.QMenu(self)
        menu = self.make_cell_context_menu(menu, row_ix, col_ix)
        menu.exec_(self.mapToGlobal(event.pos()))

    def _header_menu(self, pos):
        """Create popup menu used for header"""
        menu = QtGui.QMenu(self)
        col_ix = self.horizontalHeader().logicalIndexAt(pos)

        if col_ix == -1:
            # Out of bounds
            return

        # Filter Menu Action
        menu.addAction(DynamicFilterMenuAction(self, menu, col_ix))
        menu.addAction(FilterListMenuWidget(self, menu, col_ix))
        menu.addAction(self._icon('DialogResetButton'),
                        "Reset",
                        self._data_model.reset)

        # Sort Ascending/Decending Menu Action
        menu.addAction(self._icon('TitleBarShadeButton'),
                        "Sort Ascending",
                       partial(self._data_model.sort, col_ix=col_ix, order=QtCore.Qt.AscendingOrder))
        menu.addAction(self._icon('TitleBarUnshadeButton'),
                        "Sort Descending",
                       partial(self._data_model.sort, col_ix=col_ix, order=QtCore.Qt.DescendingOrder))
        menu.addSeparator()

        # Hide
        menu.addAction("Hide", partial(self.hideColumn, col_ix))

        # Show (column to left and right)
        for i in (-1, 1):
            if self.isColumnHidden(col_ix+i):
                menu.addAction("Show %s" % self._data_model.headerData(col_ix+i, QtCore.Qt.Horizontal),
                                partial(self.showColumn, col_ix+i))

        menu.exec_(self.mapToGlobal(pos))


    def setDataFrame(self, df):
        self._data_model.setDataFrame(df)
        self.resizeColumnsToContents()

    def filter(self, col_ix, needle):
        return self._data_model.filter(col_ix, needle)

    def filterIsIn(self, col_ix, include):
        return self._data_model.filterIsIn(col_ix, include)

    @property
    def df(self):
        return self._data_model.df
    @df.setter
    def df(self, dataFrame):
        # Use the "hard setting" of the dataframe because anyone who's interacting with the
        #  DataFrameWidget (ie, end user) would be setting this
        self._data_model.setDataFrame(dataFrame)

    def keyPressEvent(self, event):
        """Implements keyboard shortcuts"""
        if event.matches(QtGui.QKeySequence.Copy):
            self.copy()
        else:
            # Pass up
            super(DataFrameWidget, self).keyPressEvent(event)

    def copy(self):
        """Copy selected cells into copy-buffer"""
        selection = self.selectionModel()
        indexes = selection.selectedIndexes()
        if len(indexes) < 1:
            # Nothing selected
            return

        # Capture selection into a DataFrame
        items = pandas.DataFrame()
        for idx in indexes:
            row = idx.row()
            col = idx.column()
            item = idx.data()
            if item:
                items = items.set_value(row, col, str(item.toString()))

        # Make into tab-delimited text (best for Excel)
        items = list(items.itertuples(index=False))
        s = '\n'.join(['\t'.join([cell for cell in row]) for row in items])

        # Send to clipboard
        QtGui.QApplication.clipboard().setText(s)


    def _icon(self, icon_name):
        """Convinence function to get standard icons from Qt"""
        if not icon_name.startswith('SP_'):
            icon_name = 'SP_' + icon_name
        icon = getattr(QtGui.QStyle, icon_name, None)
        if icon is None:
            raise Exception("Unknown icon %s" % icon_name)
        return self.style().standardIcon(icon)

    def _on_click(self, index):
        if index.isValid():
            self.cellClicked.emit(index.row(), index.column())

    def _enable_widgeted_cells(self):
        # Update all cells with WidgetedCell to have persistent editors
        model = self.model()
        if model is None:
            return
        for r in range(model.rowCount()):
            for c in range(model.columnCount()):
                idx = model.index(r,c)
                d = model.data(idx, DataFrameModel.RawDataRole)
                if isinstance(d, WidgetedCell):
                    self.openPersistentEditor(idx)


class DataFrameApp(QtGui.QMainWindow):
    """Sample DataFrameTable Application"""
    def __init__(self, df, title="Inspecting DataFrame"):
        super(DataFrameApp, self).__init__()

        # State variables
        self.title_base = title

        # Initialize main data table
        self.table = DataFrameWidget(self)
        self.table.dataFrameChanged.connect(self.datatable_updated)
        self.table.setDataFrame(df)
        self.setCentralWidget(self.table)

        # Set window size
        col_size = sum([self.table.columnWidth(i) for i in range(0,99)])
        col_size = min(col_size+75, 1500)
        self.setGeometry(300, 300, col_size, 250)


    def datatable_updated(self):
        # Change title to reflect updated size
        df = self.table.df
        title = self.title_base + ' [%dx%d]' % (len(df.index), len(df.columns))
        self.setWindowTitle(title)

class ExampleWidgetForWidgetedCell(QtGui.QComboBox):
    """
    To implement a persistent state for the widgetd cell, you must provide
    a `getWidgetedCellState` and `setWidgetedCellState` methods.  This is how
    the WidgetedCell framework can create and destory your widget as needed.
    """
    def __init__(self, parent):
        super(ExampleWidgetForWidgetedCell, self).__init__(parent)
        self.addItem("Option A")
        self.addItem("Option B")
        self.addItem("Option C")
        self.setCurrentIndex(0)


    def getWidgetedCellState(

@tgrandje
Copy link

tgrandje commented Sep 6, 2016

Hi
Great code, thanks.
I had troubles using the filters on non-unicode string fields. I replaced the ".astype('str')" by ".astype('unicode')" and it seems to fix it.
tgrandje
(Using WinPython-32bit-2.7.10.3)

Edit :
Had also to change line in on_list_itemChanged : replaced include.append(unicode(i.text())) by include.append(str(i.text()))

@jhparkaa
Copy link

jhparkaa commented Sep 3, 2017

hi. cool code. thanks.
i need help to solve "korean" characterset encoding.
i try to converting pandas-dataframe into qtablewidget(qtableview).
i uploaded that is err-image and dataset. can you help me?

http://blog.naver.com/mr8nav3r/221088562230

@szaiser
Copy link

szaiser commented Nov 8, 2017

I did not find a way to filter for values in multiple columns at once. Setting a filter in one column unsets the on in the previous one. Is it possible to do this some way? Also some advice on how to extend DataFrameGUI.py to achive multiple column filtering would be great.

@mashkovd
Copy link

@kenza-ily
Copy link

Hey ! Does someone know how to imlpement the code in PyQt 5?

@Knhulhut
Copy link

First, thanks a lot for this code, it really helps a lot!
I think I managed to modify it to be able using it with PyQt5 and pandas==0.25.2, even though I might have missed some functionalities you implemented.
I'm creating a new fork uploading it if you're interested in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment