Skip to content

Enabled programmatic edits to grid and additional configuration options for columns and rows #191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 72 additions & 2 deletions js/src/qgrid.widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ class QgridView extends widgets.DOMWidgetView {
var columns = this.model.get('_columns');
this.data_view = this.create_data_view(df_json.data);
this.grid_options = this.model.get('grid_options');
this.column_definitions = this.model.get('column_definitions');
this.row_edit_conditions = this.model.get('row_edit_conditions');
this.index_col_name = this.model.get("_index_col_name");

this.columns = [];
Expand Down Expand Up @@ -321,7 +323,8 @@ class QgridView extends widgets.DOMWidgetView {
id: cur_column.name,
sortable: false,
resizable: true,
cssClass: cur_column.type
cssClass: cur_column.type,
toolTip: cur_column.toolTip
};

Object.assign(slick_column, type_info);
Expand All @@ -345,10 +348,17 @@ class QgridView extends widgets.DOMWidgetView {
// don't allow editing index columns
if (cur_column.is_index) {
slick_column.editor = editors.IndexEditor;
slick_column.cssClass += ' idx-col';
if (this.grid_options.boldIndex) {
slick_column.cssClass += ' idx-col';
}
this.index_columns.push(slick_column);
continue;
}

if ( ! (cur_column.editable) ) {
slick_column.editor = null;
}

this.columns.push(slick_column);
}

Expand Down Expand Up @@ -473,6 +483,60 @@ class QgridView extends widgets.DOMWidgetView {
});

// set up callbacks

// evaluate conditions under which cells in a row should be disabled (contingent on values of other cells in the same row)
var evaluateRowEditConditions = function(current_row, obj) {
var result;

for (var op in obj) {
if (op == 'AND') {
if (result == null) {
result = true;
}
for (var cond in obj[op]) {
if (cond == 'AND' || cond == 'OR' || cond == 'NOT') {
result = result && evaluateRowEditConditions(current_row, {[cond]: obj[op][cond]});
} else {
result = result && (current_row[cond] == obj[op][cond]);
}
}
} else if (op == 'OR') {
if (result == null) {
result = false;
}
var or_result = false;
for (var cond in obj[op]) {
if (cond == 'AND' || cond == 'OR' || cond == 'NAND' || cond == 'NOR') {
result = result || evaluateRowEditConditions(current_row, {[cond]: obj[op][cond]});
} else {
result = result || (current_row[cond] == obj[op][cond]);
}
}
} else if (op == 'NAND') {
if (result == null) {
result = true;
}
result = result && !evaluateRowEditConditions(current_row, {'AND': obj[op]});
} else if (op == 'NOR') {
if (result == null) {
result = false;
}
result = result || !evaluateRowEditConditions(current_row, {'OR': obj[op]});
} else {
alert("Unsupported operation '" + op + "' found in row edit conditions!")
}
}
return result;
}

if ( ! (this.row_edit_conditions == null)) {
var conditions = this.row_edit_conditions;
var grid = this.slick_grid;
this.slick_grid.onBeforeEditCell.subscribe(function(e, args) {
return evaluateRowEditConditions(grid.getDataItem(args.row), conditions);
});
}

this.slick_grid.onCellChange.subscribe((e, args) => {
var column = this.columns[args.cell].name;
var data_item = this.slick_grid.getDataItem(args.row);
Expand Down Expand Up @@ -675,6 +739,12 @@ class QgridView extends widgets.DOMWidgetView {
'type': 'selection_change'
});
}, 100);
} else if (msg.type == 'toggle_editable') {
if (this.slick_grid.getOptions().editable == false) {
this.slick_grid.setOptions({'editable': true});
} else {
this.slick_grid.setOptions({'editable': false});
}
} else if (msg.col_info) {
var filter = this.filters[msg.col_info.name];
filter.handle_msg(msg);
Expand Down
81 changes: 76 additions & 5 deletions qgrid/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ def __init__(self):
'sortable': True,
'filterable': True,
'highlightSelectedCell': False,
'highlightSelectedRow': True
'highlightSelectedRow': True,
'boldIndex': True
}
self._column_options = {
'editable': True,
'toolTip': "",
}
self._show_toolbar = False
self._precision = None # Defer to pandas.get_option
Expand All @@ -47,13 +52,15 @@ def set_grid_option(self, optname, optvalue):
self._grid_options[optname] = optvalue

def set_defaults(self, show_toolbar=None, precision=None,
grid_options=None):
grid_options=None, column_options=None):
if show_toolbar is not None:
self._show_toolbar = show_toolbar
if precision is not None:
self._precision = precision
if grid_options is not None:
self._grid_options = grid_options
if column_options is not None:
self._column_options = column_options

@property
def show_toolbar(self):
Expand All @@ -67,11 +74,15 @@ def grid_options(self):
def precision(self):
return self._precision or pd.get_option('display.precision') - 1

@property
def column_options(self):
return self._column_options


defaults = _DefaultSettings()


def set_defaults(show_toolbar=None, precision=None, grid_options=None):
def set_defaults(show_toolbar=None, precision=None, grid_options=None, column_options=None):
"""
Set the default qgrid options. The options that you can set here are the
same ones that you can pass into ``QgridWidget`` constructor, with the
Expand All @@ -94,7 +105,7 @@ def set_defaults(show_toolbar=None, precision=None, grid_options=None):
The widget whose default behavior is changed by ``set_defaults``.
"""
defaults.set_defaults(show_toolbar=show_toolbar, precision=precision,
grid_options=grid_options)
grid_options=grid_options, column_options=column_options)


def set_grid_option(optname, optvalue):
Expand Down Expand Up @@ -166,7 +177,9 @@ def disable():


def show_grid(data_frame, show_toolbar=None,
precision=None, grid_options=None):
precision=None, grid_options=None,
column_options=None, column_definitions=None,
row_edit_conditions=None):
"""
Renders a DataFrame or Series as an interactive qgrid, represented by
an instance of the ``QgridWidget`` class. The ``QgridWidget`` instance
Expand Down Expand Up @@ -196,6 +209,12 @@ def show_grid(data_frame, show_toolbar=None,
precision = defaults.precision
if not isinstance(precision, Integral):
raise TypeError("precision must be int, not %s" % type(precision))
if column_options is None:
column_options = defaults.column_options
else:
options = defaults.column_options.copy()
options.update(column_options)
column_options = options
if grid_options is None:
grid_options = defaults.grid_options
else:
Expand All @@ -215,9 +234,15 @@ def show_grid(data_frame, show_toolbar=None,
"data_frame must be DataFrame or Series, not %s" % type(data_frame)
)

row_edit_conditions = (row_edit_conditions or {})
column_definitions = (column_definitions or {})

# create a visualization for the dataframe
return QgridWidget(df=data_frame, precision=precision,
grid_options=grid_options,
column_options=column_options,
column_definitions=column_definitions,
row_edit_conditions=row_edit_conditions,
show_toolbar=show_toolbar)


Expand Down Expand Up @@ -364,6 +389,9 @@ class QgridWidget(widgets.DOMWidget):
df = Instance(pd.DataFrame)
precision = Integer(6, sync=True)
grid_options = Dict(sync=True)
column_options = Dict(sync=True)
column_definitions = Dict({})
row_edit_conditions = Dict(sync=True)
show_toolbar = Bool(False, sync=True)

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -507,6 +535,10 @@ def _update_table(self,
cur_column['position'] = i
columns[col_name] = cur_column

columns[col_name].update(self.column_options)
if col_name in self.column_definitions.keys():
columns[col_name].update(self.column_definitions[col_name])

self._columns = columns

# special handling for interval columns: convert to a string column
Expand Down Expand Up @@ -1032,6 +1064,45 @@ def add_row(self):
scroll_to_row=df.index.get_loc(last.name))
self._trigger_df_change_event()

def add_row_internally(self, row):
"""
Append a new row to the end of the dataframe given a list of 2-tuples of (column name, column value).
This feature will work for dataframes with arbitrary index types.
"""
df = self._df

col_names, col_data = zip(*row)
col_names = list(col_names)
col_data = list(col_data)
index_col_val = dict(row)[df.index.name]

# check that the given column names match what already exists in the dataframe
required_cols = set(df.columns.values).union({df.index.name}) - {self._index_col_name}
if set(col_names) != required_cols:
msg = "Cannot add row -- column names don't match in the existing dataframe"
self.send({
'type': 'show_error',
'error_msg': msg,
'triggered_by': 'add_row'
})
return

for i, s in enumerate(col_data):
if col_names[i] == df.index.name:
continue

df.loc[index_col_val, col_names[i]] = s
self._unfiltered_df.loc[index_col_val, col_names[i]] = s

self._update_table(triggered_by='add_row', scroll_to_row=df.index.get_loc(index_col_val), fire_data_change_event=True)
self._trigger_df_change_event()

def set_value_internally(self, index, column, value):
self._df.loc[index, column] = value
self._unfiltered_df.loc[index, column] = value
self._update_table(triggered_by='cell_change', fire_data_change_event=True)
self._trigger_df_change_event()

def remove_row(self):
"""
Remove the current row from the table.
Expand Down
30 changes: 30 additions & 0 deletions qgrid/tests/test_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,3 +409,33 @@ def test_object_dtype_categorical():
})
assert len(widget._df) == 1
assert widget._df[0][0] == cat_series[0]


def test_add_row_internally():
df = pd.DataFrame({'foo': ['hello'], 'bar': ['world'], 'baz': [42], 'boo': [57]})
df.set_index('baz', inplace=True, drop=True)

q = QgridWidget(df=df)

new_row = [
('baz', 43),
('bar', "new bar"),
('boo', 58),
('foo', "new foo")
]

q.add_row_internally(new_row)

assert q._df.loc[43, 'foo'] == 'new foo'
assert q._df.loc[42, 'foo'] == 'hello'


def test_set_value_internally():
df = pd.DataFrame({'foo': ['hello'], 'bar': ['world'], 'baz': [42], 'boo': [57]})
df.set_index('baz', inplace=True, drop=True)

q = QgridWidget(df=df)

q.set_value_internally(42, 'foo', 'hola')

assert q._df.loc[42, 'foo'] == 'hola'