diff --git a/js/src/qgrid.widget.js b/js/src/qgrid.widget.js index 9d9b8fbf..efa1417c 100644 --- a/js/src/qgrid.widget.js +++ b/js/src/qgrid.widget.js @@ -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 = []; @@ -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); @@ -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); } @@ -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); @@ -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); diff --git a/qgrid/grid.py b/qgrid/grid.py index 62105730..5b4e9834 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -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 @@ -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): @@ -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 @@ -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): @@ -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 @@ -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: @@ -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) @@ -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): @@ -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 @@ -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. diff --git a/qgrid/tests/test_grid.py b/qgrid/tests/test_grid.py index 3a73aad7..158b3e3e 100644 --- a/qgrid/tests/test_grid.py +++ b/qgrid/tests/test_grid.py @@ -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'