From 9e03209a0cac4b18355ebf5df7c2182aa1c02a1e Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Mon, 8 Jan 2018 11:29:43 -0500 Subject: [PATCH 1/9] Make page size a module-level constant and bump the version to 1.0.1a2. --- js/package.json | 2 +- js/src/qgrid.widget.js | 4 ++-- qgrid/_version.py | 2 +- qgrid/grid.py | 17 +++++++++-------- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/js/package.json b/js/package.json index 8f174eb3..3825ecb9 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "qgrid", - "version": "1.0.0", + "version": "1.0.1-alpha.2", "description": "An Interactive Grid for Sorting and Filtering DataFrames in Jupyter Notebook", "author": "Quantopian Inc.", "main": "src/index.js", diff --git a/js/src/qgrid.widget.js b/js/src/qgrid.widget.js index f60863dc..a81320aa 100644 --- a/js/src/qgrid.widget.js +++ b/js/src/qgrid.widget.js @@ -36,8 +36,8 @@ class QgridModel extends widgets.DOMWidgetModel { _view_name : 'QgridView', _model_module : 'qgrid', _view_module : 'qgrid', - _model_module_version : '^1.0.0', - _view_module_version : '^1.0.0', + _model_module_version : '^1.0.1-alpha.2', + _view_module_version : '^1.0.1-alpha.2', _df_json: '', _columns: {} }); diff --git a/qgrid/_version.py b/qgrid/_version.py index e1b56266..337fd37b 100644 --- a/qgrid/_version.py +++ b/qgrid/_version.py @@ -1,4 +1,4 @@ -version_info = (1, 0, 0, 'final') +version_info = (1, 0, 1, 'alpha', 2) _specifier_ = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc', 'final': ''} diff --git a/qgrid/grid.py b/qgrid/grid.py index 9bc9340a..202d0953 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -216,6 +216,8 @@ def show_grid(data_frame, show_toolbar=None, show_toolbar=show_toolbar) +PAGE_SIZE = 100 + @widgets.register() class QgridWidget(widgets.DOMWidget): """ @@ -307,12 +309,11 @@ class QgridWidget(widgets.DOMWidget): _model_name = Unicode('QgridModel').tag(sync=True) _view_module = Unicode('qgrid').tag(sync=True) _model_module = Unicode('qgrid').tag(sync=True) - _view_module_version = Unicode('1.0.0').tag(sync=True) - _model_module_version = Unicode('1.0.0').tag(sync=True) + _view_module_version = Unicode('1.0.1-alpha.2').tag(sync=True) + _model_module_version = Unicode('1.0.1-alpha.2').tag(sync=True) _df = Instance(pd.DataFrame) _df_json = Unicode('', sync=True) - _page_size = Integer(100, sync=True) _primary_key = List() _columns = Dict({}, sync=True) _filter_tables = Dict({}) @@ -403,8 +404,8 @@ def _update_table(self, update_columns=False, triggered_by=None, scroll_to_row=None): df = self._df.copy() - from_index = max(self._viewport_range[0] - self._page_size, 0) - to_index = max(self._viewport_range[0] + self._page_size, 0) + from_index = max(self._viewport_range[0] - PAGE_SIZE, 0) + to_index = max(self._viewport_range[0] + PAGE_SIZE, 0) new_df_range = (from_index, to_index) if triggered_by is 'viewport_changed' and \ @@ -724,7 +725,7 @@ def get_value_from_filter_table(k): if col_info['type'] == 'any': col_info['value_range'] = (0, length) else: - max_items = self._page_size * 2 + max_items = PAGE_SIZE * 2 range_max = length if length > max_items: col_info['values'] = col_info['values'][:max_items] @@ -910,8 +911,8 @@ def _handle_qgrid_msg_helper(self, content): col_info = self._columns[col_name] col_filter_table = self._filter_tables[col_name] - from_index = max(content['top'] - self._page_size, 0) - to_index = max(content['top'] + self._page_size, 0) + from_index = max(content['top'] - PAGE_SIZE, 0) + to_index = max(content['top'] + PAGE_SIZE, 0) col_info['values'] = col_filter_table[from_index:to_index] col_info['value_range'] = (from_index, to_index) From 3225c881510ff4a4befe816edf2fa89e6fbb1dcd Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Sat, 20 Jan 2018 17:48:19 -0500 Subject: [PATCH 2/9] Fix issue where installing qgrid would break bqplot on jupyterlab. Bump version to 1.0.1 beta 0. --- js/package.json | 2 +- js/src/index.js | 8 -------- js/src/qgrid.widget.js | 4 ++-- qgrid/_version.py | 2 +- qgrid/grid.py | 4 ++-- 5 files changed, 6 insertions(+), 14 deletions(-) diff --git a/js/package.json b/js/package.json index 3825ecb9..093a7afc 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "qgrid", - "version": "1.0.1-alpha.2", + "version": "1.0.1-beta.0", "description": "An Interactive Grid for Sorting and Filtering DataFrames in Jupyter Notebook", "author": "Quantopian Inc.", "main": "src/index.js", diff --git a/js/src/index.js b/js/src/index.js index 874bc497..65a87ddf 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -1,12 +1,4 @@ // Entry point for the notebook bundle containing custom model definitions. -// -// Setup notebook base URL -// -// Some static assets may be required by the custom widget javascript. The base -// url for the notebook is not known at build time and is therefore computed -// dynamically. -__webpack_public_path__ = document.querySelector('body').getAttribute('data-base-url') + 'nbextensions/qgrid/'; - // Export widget models and views, and the npm package version number. module.exports = require('./qgrid.widget.js'); module.exports.version = require('../package.json').version; diff --git a/js/src/qgrid.widget.js b/js/src/qgrid.widget.js index a81320aa..73ce6e66 100644 --- a/js/src/qgrid.widget.js +++ b/js/src/qgrid.widget.js @@ -36,8 +36,8 @@ class QgridModel extends widgets.DOMWidgetModel { _view_name : 'QgridView', _model_module : 'qgrid', _view_module : 'qgrid', - _model_module_version : '^1.0.1-alpha.2', - _view_module_version : '^1.0.1-alpha.2', + _model_module_version : '^1.0.1-beta.0', + _view_module_version : '^1.0.1-beta.0', _df_json: '', _columns: {} }); diff --git a/qgrid/_version.py b/qgrid/_version.py index 337fd37b..93a1614d 100644 --- a/qgrid/_version.py +++ b/qgrid/_version.py @@ -1,4 +1,4 @@ -version_info = (1, 0, 1, 'alpha', 2) +version_info = (1, 0, 1, 'beta', 0) _specifier_ = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc', 'final': ''} diff --git a/qgrid/grid.py b/qgrid/grid.py index 202d0953..8acc22dd 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -309,8 +309,8 @@ class QgridWidget(widgets.DOMWidget): _model_name = Unicode('QgridModel').tag(sync=True) _view_module = Unicode('qgrid').tag(sync=True) _model_module = Unicode('qgrid').tag(sync=True) - _view_module_version = Unicode('1.0.1-alpha.2').tag(sync=True) - _model_module_version = Unicode('1.0.1-alpha.2').tag(sync=True) + _view_module_version = Unicode('1.0.1-beta.0').tag(sync=True) + _model_module_version = Unicode('1.0.1-beta.0').tag(sync=True) _df = Instance(pd.DataFrame) _df_json = Unicode('', sync=True) From 29dc946d8bd8b48c66462559d2b030b6c462e38e Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Sat, 20 Jan 2018 23:34:45 -0500 Subject: [PATCH 3/9] Convert the values in categorical type columns to strings before serializing to json. Add tests to assert object dtype values are always converted to strings before serializing to json and storing on widget attributes. --- qgrid/grid.py | 4 +-- qgrid/tests/test_grid.py | 66 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/qgrid/grid.py b/qgrid/grid.py index 8acc22dd..fdd5f5d3 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -425,7 +425,7 @@ def _update_table(self, update_columns=False, triggered_by=None, if update_columns: self._string_columns = list(df.select_dtypes( - include=[np.dtype('O')] + include=[np.dtype('O'), 'category'] ).columns.values) # call map(str) for all columns identified as string columns, in @@ -650,7 +650,7 @@ def _handle_get_column_min_max(self, content): return else: if col_info['type'] == 'any': - unique_list = col_info['constraints']['enum'] + unique_list = col_series.dtype.categories else: if col_name in self._sorted_column_cache: unique_list = self._sorted_column_cache[col_name] diff --git a/qgrid/tests/test_grid.py b/qgrid/tests/test_grid.py index 56f84e07..e16ebb6f 100644 --- a/qgrid/tests/test_grid.py +++ b/qgrid/tests/test_grid.py @@ -5,6 +5,7 @@ ) import numpy as np import pandas as pd +import json def create_df(): return pd.DataFrame({ @@ -274,3 +275,68 @@ def assert_widget_vals_b(widget): view = QgridWidget(df=df) assert_widget_vals_b(view) + +class MyObject(object): + def __init__(self, obj): + self.obj = obj + + +my_object_vals = [MyObject(MyObject(None)), MyObject(None)] + + +def test_object_dtype(): + df = pd.DataFrame({'a': my_object_vals}) + widget = QgridWidget(df=df) + grid_data = json.loads(widget._df_json)['data'] + + widget._handle_qgrid_msg_helper({ + 'type': 'get_column_min_max', + 'field': 'a', + 'search_val': None + }) + widget._handle_qgrid_msg_helper({ + 'field': "a", + 'filter_info': { + 'field': "a", + 'selected': [0, 1], + 'type': "text", + 'excluded': [] + }, + 'type': "filter_changed" + }) + + filter_table = widget._filter_tables['a'] + assert not isinstance(filter_table[0], dict) + assert not isinstance(filter_table[1], dict) + + assert not isinstance(grid_data[0]['a'], dict) + assert not isinstance(grid_data[1]['a'], dict) + + +def test_object_dtype_categorical(): + cat_series = pd.Series( + pd.Categorical(my_object_vals, + categories=my_object_vals) + ) + widget = show_grid(cat_series) + constraints_enum = widget._columns[0]['constraints']['enum'] + assert not isinstance(constraints_enum[0], dict) + assert not isinstance(constraints_enum[1], dict) + + widget._handle_qgrid_msg_helper({ + 'type': 'get_column_min_max', + 'field': 0, + 'search_val': None + }) + widget._handle_qgrid_msg_helper({ + 'field': 0, + 'filter_info': { + 'field': 0, + 'selected': [0], + 'type': "text", + 'excluded': [] + }, + 'type': "filter_changed" + }) + assert len(widget._df) == 1 + assert widget._df[0][0] == cat_series[0] From 24ea609c696381019d0266cabdbf2ffd5eb13f87 Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Sun, 28 Jan 2018 22:27:02 -0500 Subject: [PATCH 4/9] Adding options for disabling sorting/filtering, also for choosing whether to highlight the selected cell, selected row, both, or neither. --- js/src/qgrid.css | 45 ++++++++++++++++++++++++++--------------- js/src/qgrid.editors.js | 6 +++--- js/src/qgrid.widget.js | 32 ++++++++++++++++++++++------- qgrid/grid.py | 37 ++++++++++++++++++++++++++------- 4 files changed, 87 insertions(+), 33 deletions(-) diff --git a/js/src/qgrid.css b/js/src/qgrid.css index 088a52fb..b7234d36 100644 --- a/js/src/qgrid.css +++ b/js/src/qgrid.css @@ -284,8 +284,8 @@ } .q-grid .slick-cell { - border-bottom: 1px solid #e1e8ed !important; - border-right: none !important; + border-bottom: 1px solid #e1e8ed; + border-right: none; border-top: 1px solid transparent; border-left: 1px solid transparent; font-size: 13px; @@ -294,6 +294,32 @@ padding-left: 0px; } +.q-grid.highlight-selected-row .slick-cell.selected { + background-color: #deeaf7; +} + +.q-grid.highlight-selected-cell .slick-cell.active { + border: 2px solid rgb(65, 165, 245); + padding-top: 2px; + padding-bottom: 1px; + padding-left: 3px; +} + +.q-grid .slick-cell.editable:not(.idx-col) { + border: 2px solid rgb(65, 165, 245); + padding-top: 2px; + padding-bottom: 1px; + padding-left: 3px !important; + background-color: #FFF; + -webkit-box-shadow: 0 2px 5px rgba(0,0,0,0.4); + -moz-box-shadow: 0 2px 5px rgba(0,0,0,0.4); + box-shadow: 0 2px 5px rgba(0,0,0,0.4); +} + +.q-grid .slick-cell.editable .editor-select:focus { + outline-style: none; +} + .q-grid .slick-cell.idx-col { font-weight: bold; } @@ -304,7 +330,7 @@ } .q-grid .slick-cell.selected { - background-color: #deeaf7; + background-color: transparent; } /* Filter button */ @@ -605,18 +631,6 @@ input.bool-filter-radio { margin-left: -23px; } -.slick-cell.editable:not(.idx-col) { - background-color: rgb(255, 247, 141) !important; - border: 1px solid rgba(0, 0, 0, 0.26) !important; - margin-top: -1px; - padding-top: 4px; - padding-bottom: 2px; -} - -.slick-cell.editable:not(:last-child):not(.idx-col) { - margin-right: 3px; -} - .slick-cell { -webkit-touch-callout: none; /* iOS Safari */ -webkit-user-select: none; /* Safari */ @@ -627,6 +641,5 @@ input.bool-filter-radio { } .slick-row .slick-cell:not(:first-child) { - margin-left: -4px; padding-left: 4px; } diff --git a/js/src/qgrid.editors.js b/js/src/qgrid.editors.js index 89ba4a98..502efeff 100644 --- a/js/src/qgrid.editors.js +++ b/js/src/qgrid.editors.js @@ -16,11 +16,11 @@ class IndexEditor { this.$cell.tooltip(); this.$cell.tooltip('enable'); this.$cell.tooltip("open"); - this.$cell.off('tooltipclose'); - this.$cell.on("tooltipclose", (event, ui) => { + // automatically hide it after 4 seconds + setTimeout((event, ui) => { this.$cell.tooltip('destroy'); args.cancelChanges(); - }); + }, 3000); } destroy() {} diff --git a/js/src/qgrid.widget.js b/js/src/qgrid.widget.js index 73ce6e66..d5b19a50 100644 --- a/js/src/qgrid.widget.js +++ b/js/src/qgrid.widget.js @@ -370,6 +370,16 @@ class QgridView extends widgets.DOMWidgetView { this.grid_elem.addClass('force-fit-columns'); } + if (this.grid_options.highlightSelectedCell) { + this.grid_elem.addClass('highlight-selected-cell'); + } + + // compare to false since we still want to show row + // selection if this option is excluded entirely + if (this.grid_options.highlightSelectedRow != false) { + this.grid_elem.addClass('highlight-selected-row'); + } + setTimeout(() => { this.slick_grid.init(); this.update_size(); @@ -378,12 +388,16 @@ class QgridView extends widgets.DOMWidgetView { this.slick_grid.setSelectionModel(new Slick.RowSelectionModel()); this.slick_grid.render(); - this.slick_grid.onHeaderCellRendered.subscribe((e, args) => { + var render_header_cell = (e, args) => { var cur_filter = this.filters[args.column.id]; - if (cur_filter){ - cur_filter.render_filter_button($(args.node), this.slick_grid); - } - }); + if (cur_filter) { + cur_filter.render_filter_button($(args.node), this.slick_grid); + } + }; + + if (this.grid_options.filterable != false) { + this.slick_grid.onHeaderCellRendered.subscribe(render_header_cell); + } // Force the grid to re-render the column headers so the // onHeaderCellRendered event is triggered. @@ -396,7 +410,7 @@ class QgridView extends widgets.DOMWidgetView { this.slick_grid.setSortColumns([]); this.grid_header = this.$el.find('.slick-header-columns'); - this.grid_header.click((e) => { + var handle_header_click = (e) => { if (this.resizing_column) { return; } @@ -436,7 +450,11 @@ class QgridView extends widgets.DOMWidgetView { 'sort_ascending': this.sort_ascending }; this.send(msg); - }); + }; + + if (this.grid_options.sortable != false) { + this.grid_header.click(handle_header_click) + } this.slick_grid.onViewportChanged.subscribe((e) => { if (this.viewport_timeout){ diff --git a/qgrid/grid.py b/qgrid/grid.py index fdd5f5d3..b0da9f9b 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -33,7 +33,11 @@ def __init__(self): 'autoEdit': False, 'explicitInitialization': True, 'maxVisibleRows': 15, - 'minVisibleRows': 8 + 'minVisibleRows': 8, + 'sortable': True, + 'filterable': True, + 'highlightSelectedCell': False, + 'highlightSelectedRow': True } self._show_toolbar = False self._precision = None # Defer to pandas.get_option @@ -266,17 +270,36 @@ class QgridWidget(widgets.DOMWidget): 'autoEdit': False, 'explicitInitialization': True, 'maxVisibleRows': 15, - 'minVisibleRows': 8 + 'minVisibleRows': 8, + 'sortable': True, + 'filterable': True, + 'highlightSelectedCell': False, + 'highlightSelectedRow': True } Most of these options are SlickGrid options which are described in the `SlickGrid documentation `_. The - two exceptions are `maxVisibleRows` and `minVisibleRows`, which - are options that were added specifically for Qgrid and therefore - are not documented in the SlickGrid documentation. These options - allow you to set an upper and lower bound on the height of your - Qgrid widget in terms of number of rows that are visible. + exceptions are the last 6 options listed, which are options that were + added specifically for Qgrid and therefore are not documented in the + SlickGrid documentation. + + The first two, `maxVisibleRows` and `minVisibleRows`, allow you to set + an upper and lower bound on the height of your Qgrid widget in terms of + number of rows that are visible. + + The next two, `sortable` and `filterable`, control whether qgrid will + allow the user to sort and filter, respectively. If you set `sortable` to + False nothing will happen when the column headers are clicked. + If you set `filterable` to False, the filter icons won't be shown for any + columns. + + The last two, `highlightSelectedCell` and `highlightSelectedRow`, control + how the styling of qgrid changes when a cell is selected. If you set + `highlightSelectedCell` to True, the selected cell will be given + a light blue border. If you set `highlightSelectedRow` to False, the + light blue background that's shown by default for selected rows will be + hidden. See Also -------- From f0194228197d049d760a67ef69fba9ab85688b6d Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Sun, 28 Jan 2018 23:00:19 -0500 Subject: [PATCH 5/9] Bump to beta 1 --- js/package.json | 2 +- js/src/qgrid.widget.js | 4 ++-- qgrid/_version.py | 2 +- qgrid/grid.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/js/package.json b/js/package.json index 093a7afc..67600dfc 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "qgrid", - "version": "1.0.1-beta.0", + "version": "1.0.1-beta.1", "description": "An Interactive Grid for Sorting and Filtering DataFrames in Jupyter Notebook", "author": "Quantopian Inc.", "main": "src/index.js", diff --git a/js/src/qgrid.widget.js b/js/src/qgrid.widget.js index d5b19a50..f7b9fa0c 100644 --- a/js/src/qgrid.widget.js +++ b/js/src/qgrid.widget.js @@ -36,8 +36,8 @@ class QgridModel extends widgets.DOMWidgetModel { _view_name : 'QgridView', _model_module : 'qgrid', _view_module : 'qgrid', - _model_module_version : '^1.0.1-beta.0', - _view_module_version : '^1.0.1-beta.0', + _model_module_version : '^1.0.1-beta.1', + _view_module_version : '^1.0.1-beta.1', _df_json: '', _columns: {} }); diff --git a/qgrid/_version.py b/qgrid/_version.py index 93a1614d..36ea4aee 100644 --- a/qgrid/_version.py +++ b/qgrid/_version.py @@ -1,4 +1,4 @@ -version_info = (1, 0, 1, 'beta', 0) +version_info = (1, 0, 1, 'beta', 1) _specifier_ = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc', 'final': ''} diff --git a/qgrid/grid.py b/qgrid/grid.py index b0da9f9b..fc124dfd 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -332,8 +332,8 @@ class QgridWidget(widgets.DOMWidget): _model_name = Unicode('QgridModel').tag(sync=True) _view_module = Unicode('qgrid').tag(sync=True) _model_module = Unicode('qgrid').tag(sync=True) - _view_module_version = Unicode('1.0.1-beta.0').tag(sync=True) - _model_module_version = Unicode('1.0.1-beta.0').tag(sync=True) + _view_module_version = Unicode('1.0.1-beta.1').tag(sync=True) + _model_module_version = Unicode('1.0.1-beta.1').tag(sync=True) _df = Instance(pd.DataFrame) _df_json = Unicode('', sync=True) From 61b859fb65f3559058b770d15ebdc5bb0148d774 Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Mon, 5 Feb 2018 09:07:36 -0500 Subject: [PATCH 6/9] Handle resize event in jupyterlab. --- js/src/qgrid.widget.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/js/src/qgrid.widget.js b/js/src/qgrid.widget.js index f7b9fa0c..57086e9f 100644 --- a/js/src/qgrid.widget.js +++ b/js/src/qgrid.widget.js @@ -509,6 +509,18 @@ class QgridView extends widgets.DOMWidgetView { }, 1); } + processPhosphorMessage(msg) { + super.processPhosphorMessage(msg) + switch (msg.type) { + case 'resize': + case 'after-show': + if (this.slick_grid){ + this.slick_grid.resizeCanvas(); + } + break; + } + } + has_active_filter() { for (var i=0; i < this.filter_list.length; i++){ var cur_filter = this.filter_list[i]; From de6c657c2c875c56e8db8faaabbf15e714c66a90 Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Sun, 11 Feb 2018 01:52:22 -0500 Subject: [PATCH 7/9] Trigger change event for _df traitlet for editing, sorting. --- qgrid/grid.py | 14 ++++++++++++++ qgrid/tests/test_grid.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/qgrid/grid.py b/qgrid/grid.py index fc124dfd..a010cb76 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -6,6 +6,7 @@ from IPython.display import display from numbers import Integral from traitlets import Unicode, Instance, Bool, Integer, Dict, List, Tuple, Any +from traitlets.utils.bunch import Bunch # versions of pandas prior to version 0.20.0 don't support the orient='table' # when calling the 'to_json' function on DataFrames. to get around this we @@ -909,6 +910,7 @@ def _handle_qgrid_msg_helper(self, content): query = self._unfiltered_df[self._index_col_name] == \ content['unfiltered_index'] self._unfiltered_df.loc[query, content['column']] = val_to_set + self._trigger_df_change_event() except (ValueError, TypeError): msg = "Error occurred while attempting to edit the " \ "DataFrame. Check the notebook server logs for more " \ @@ -951,11 +953,21 @@ def _handle_qgrid_msg_helper(self, content): self._sorted_column_cache = {} self._update_sort() self._update_table(triggered_by='sort_changed') + self._trigger_df_change_event() elif content['type'] == 'get_column_min_max': self._handle_get_column_min_max(content) elif content['type'] == 'filter_changed': self._handle_filter_changed(content) + def _trigger_df_change_event(self): + self.notify_change(Bunch( + name='_df', + old=None, + new=self._df, + owner=self, + type='change', + )) + def get_changed_df(self): """ Get a copy of the DataFrame that was used to create the current @@ -1014,6 +1026,7 @@ def add_row(self): self._unfiltered_df.loc[last.name] = last.values self._update_table(triggered_by='add_row', scroll_to_row=df.index.get_loc(last.name)) + self._trigger_df_change_event() def remove_row(self): """ @@ -1033,6 +1046,7 @@ def remove_row(self): self._unfiltered_df.drop(selected_names, inplace=True) self._selected_rows = [] self._update_table(triggered_by='remove_row') + self._trigger_df_change_event() # Alias for legacy support, since we changed the capitalization diff --git a/qgrid/tests/test_grid.py b/qgrid/tests/test_grid.py index e16ebb6f..03ce7c48 100644 --- a/qgrid/tests/test_grid.py +++ b/qgrid/tests/test_grid.py @@ -38,6 +38,15 @@ def create_interval_index_df(): def test_edit_date(): view = QgridWidget(df=create_df()) + observer_called = False + + def on_value_change(change): + nonlocal observer_called + observer_called = True + assert change['new']['Date'][3] == pd.Timestamp('2013-01-16 00:00:00') + + view.observe(on_value_change, names=['_df']) + view._handle_qgrid_msg_helper({ 'column': "Date", 'row_index': 3, @@ -46,12 +55,25 @@ def test_edit_date(): 'value': "2013-01-16T00:00:00.000+00:00" }) + assert observer_called + def test_add_row(): view = QgridWidget(df=create_df()) + + observer_called = False + def on_value_change(change): + nonlocal observer_called + observer_called = True + assert len(change['new']) == 5 + + view.observe(on_value_change, names=['_df']) + view._handle_qgrid_msg_helper({ 'type': 'add_row' }) + assert observer_called + def test_mixed_type_column(): df = pd.DataFrame({'A': [1.2, 'xy', 4], 'B': [3, 4, 5]}) df = df.set_index(pd.Index(['yz', 7, 3.2])) @@ -202,6 +224,13 @@ def test_date_index(): def test_multi_index(): view = QgridWidget(df=create_multi_index_df()) + observer_count = 0 + def on_value_change(change): + nonlocal observer_count + observer_count += 1 + + view.observe(on_value_change, names=['_df']) + view._handle_qgrid_msg_helper({ 'type': 'get_column_min_max', 'field': 'level_0', @@ -231,6 +260,8 @@ def test_multi_index(): 'sort_ascending': True }) + assert observer_count == 3 + def test_interval_index(): df = create_interval_index_df() df.set_index('time_bin', inplace=True) From 0192028f58820045e52b1c7386c1b8a2c9c1f0fb Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Sun, 11 Feb 2018 02:00:54 -0500 Subject: [PATCH 8/9] =?UTF-8?q?Fix=20an=20issue=20with=20the=20text=20filt?= =?UTF-8?q?er=20where=20the=20search=20results=20wouldn=E2=80=99t=20be=20r?= =?UTF-8?q?eset=20when=20the=20reset=20filter=20link=20was=20clicked.=20?= =?UTF-8?q?=20Also=20it=20was=20double=20firing=20the=20filter=5Fchanged?= =?UTF-8?q?=20event=20unnecessarily.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/src/qgrid.booleanfilter.js | 1 + js/src/qgrid.datefilter.js | 1 + js/src/qgrid.filterbase.js | 20 ++------------------ js/src/qgrid.sliderfilter.js | 1 + js/src/qgrid.textfilter.js | 9 +++++++++ qgrid/tests/test_grid.py | 13 ++++++++++++- 6 files changed, 26 insertions(+), 19 deletions(-) diff --git a/js/src/qgrid.booleanfilter.js b/js/src/qgrid.booleanfilter.js index 04a61a68..574306d2 100644 --- a/js/src/qgrid.booleanfilter.js +++ b/js/src/qgrid.booleanfilter.js @@ -79,6 +79,7 @@ class BooleanFilter extends filter_base.FilterBase { reset_filter() { this.radio_buttons.prop('checked', false); this.selected = null; + this.send_filter_changed(); } get_filter_info() { diff --git a/js/src/qgrid.datefilter.js b/js/src/qgrid.datefilter.js index d4ee941c..dc49b780 100644 --- a/js/src/qgrid.datefilter.js +++ b/js/src/qgrid.datefilter.js @@ -51,6 +51,7 @@ class DateFilter extends filter_base.FilterBase { this.filter_start_date = null; this.filter_end_date = null; + this.send_filter_changed(); } initialize_controls() { diff --git a/js/src/qgrid.filterbase.js b/js/src/qgrid.filterbase.js index 997faa19..47b41402 100644 --- a/js/src/qgrid.filterbase.js +++ b/js/src/qgrid.filterbase.js @@ -152,10 +152,10 @@ class FilterBase { initialize_controls() { this.filter_elem.find("a.reset-link").click( - (e) => this.handle_reset_filter_clicked(e) + (e) => this.reset_filter() ); this.filter_elem.find("i.close-button").click( - (e) => this.handle_close_button_clicked(e) + (e) => this.hide_filter() ); $(document.body).bind("mousedown", (e) => this.handle_body_mouse_down(e) @@ -180,22 +180,6 @@ class FilterBase { this.widget_model.send(msg); } - handle_reset_filter_clicked(e) { - this.reset_filter(); - this.send_filter_changed(); - // The "false" parameter tells backtest_table_manager that we want to recalculate the min/max values for this filter - // based on the rows that are still included in the grid. This is because if this filter was already active, - // its min/max could be out-of-date because we don't adjust the min/max on active filters (to prevent confusion). - // This is currently the only filter_changed case where it's appropriate to have this filter's min/max recalculated, - // because you wouldn't want to adjust a slider's min/max while the user was moving the slider, for example. - return false; - } - - handle_close_button_clicked(e) { - this.hide_filter(); - return false; - } - handle_body_mouse_down(e) { if (this.filter_elem && this.filter_elem[0] != e.target && !$.contains(this.filter_elem[0], e.target) && !$.contains(this.filter_btn[0], e.target) && diff --git a/js/src/qgrid.sliderfilter.js b/js/src/qgrid.sliderfilter.js index 31837e93..82517489 100644 --- a/js/src/qgrid.sliderfilter.js +++ b/js/src/qgrid.sliderfilter.js @@ -100,6 +100,7 @@ class SliderFilter extends filter_base.FilterBase { }); this.set_value(this.min_value, this.max_value); } + this.send_filter_changed(); } is_active() { diff --git a/js/src/qgrid.textfilter.js b/js/src/qgrid.textfilter.js index 9069b7a3..bc66fb9f 100644 --- a/js/src/qgrid.textfilter.js +++ b/js/src/qgrid.textfilter.js @@ -355,11 +355,20 @@ class TextFilter extends filter_base.FilterBase { } reset_filter() { + this.ignore_selection_changed = true; this.search_string = ""; this.excluded_rows = null; this.security_search.val(""); this.row_selection_model.setSelectedRows([]); this.filter_list = null; + this.send_filter_changed(); + var msg = { + 'type': 'get_column_min_max', + 'field': this.field, + 'search_val': this.search_string + }; + this.widget_model.send(msg); + this.ignore_selection_changed = false; } get_filter_info() { diff --git a/qgrid/tests/test_grid.py b/qgrid/tests/test_grid.py index 03ce7c48..5c486366 100644 --- a/qgrid/tests/test_grid.py +++ b/qgrid/tests/test_grid.py @@ -248,6 +248,17 @@ def on_value_change(change): } }) + view._handle_qgrid_msg_helper({ + 'type': 'filter_changed', + 'field': 3, + 'filter_info': { + 'field': 3, + 'type': 'slider', + 'min': None, + 'max': None + } + }) + view._handle_qgrid_msg_helper({ 'type': 'sort_changed', 'sort_field': 3, @@ -260,7 +271,7 @@ def on_value_change(change): 'sort_ascending': True }) - assert observer_count == 3 + assert observer_count == 4 def test_interval_index(): df = create_interval_index_df() From 7127b224c43357a71633bbf4f5a8c3019041d471 Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Sun, 18 Feb 2018 12:57:36 -0500 Subject: [PATCH 9/9] Bump to version 1.0.1 --- js/package.json | 2 +- js/src/qgrid.widget.js | 4 ++-- qgrid/_version.py | 2 +- qgrid/grid.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/js/package.json b/js/package.json index 67600dfc..2bac0205 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "qgrid", - "version": "1.0.1-beta.1", + "version": "1.0.1", "description": "An Interactive Grid for Sorting and Filtering DataFrames in Jupyter Notebook", "author": "Quantopian Inc.", "main": "src/index.js", diff --git a/js/src/qgrid.widget.js b/js/src/qgrid.widget.js index 57086e9f..b2b60d81 100644 --- a/js/src/qgrid.widget.js +++ b/js/src/qgrid.widget.js @@ -36,8 +36,8 @@ class QgridModel extends widgets.DOMWidgetModel { _view_name : 'QgridView', _model_module : 'qgrid', _view_module : 'qgrid', - _model_module_version : '^1.0.1-beta.1', - _view_module_version : '^1.0.1-beta.1', + _model_module_version : '^1.0.1', + _view_module_version : '^1.0.1', _df_json: '', _columns: {} }); diff --git a/qgrid/_version.py b/qgrid/_version.py index 36ea4aee..ef015a50 100644 --- a/qgrid/_version.py +++ b/qgrid/_version.py @@ -1,4 +1,4 @@ -version_info = (1, 0, 1, 'beta', 1) +version_info = (1, 0, 1, 'final') _specifier_ = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc', 'final': ''} diff --git a/qgrid/grid.py b/qgrid/grid.py index a010cb76..296c1d45 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -333,8 +333,8 @@ class QgridWidget(widgets.DOMWidget): _model_name = Unicode('QgridModel').tag(sync=True) _view_module = Unicode('qgrid').tag(sync=True) _model_module = Unicode('qgrid').tag(sync=True) - _view_module_version = Unicode('1.0.1-beta.1').tag(sync=True) - _model_module_version = Unicode('1.0.1-beta.1').tag(sync=True) + _view_module_version = Unicode('1.0.1').tag(sync=True) + _model_module_version = Unicode('1.0.1').tag(sync=True) _df = Instance(pd.DataFrame) _df_json = Unicode('', sync=True)