From 689e2029fa1fbe0dddeff56bb3ed3a7c08794173 Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Sat, 9 Jun 2018 23:55:41 -0400 Subject: [PATCH 1/9] Call 'str' on index columns that have an object or categorical dtype (we were already doing this on non-index columns). --- qgrid/grid.py | 32 ++++++++++++++++++++++++++------ qgrid/tests/test_grid.py | 17 ++++++++++++++++- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/qgrid/grid.py b/qgrid/grid.py index 94802fce..c59902be 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -748,16 +748,36 @@ def _update_table(self, self._row_count = len(self._df.index) - if type(df.index) == pd.core.index.MultiIndex: - self._multi_index = True - else: - self._multi_index = False - if update_columns: self._string_columns = list(df.select_dtypes( include=[np.dtype('O'), 'category'] ).columns.values) + def should_be_stringified(col_series): + return col_series.dtype == np.dtype('O') or \ + hasattr(col_series, 'cat') + + if type(df.index) == pd.core.index.MultiIndex: + self._multi_index = True + for idx, cur_level in enumerate(df.index.levels): + if cur_level.name: + col_name = cur_level.name + else: + col_name = 'level_%s' % idx + self._primary_key.append(col_name) + if should_be_stringified(cur_level): + self._string_columns.append(col_name) + else: + self._multi_index = False + if df.index.name: + col_name = df.index.name + else: + col_name = 'index' + self._primary_key = [col_name] + + if should_be_stringified(df.index): + self._string_columns.append(col_name) + # call map(str) for all columns identified as string columns, in # case any are not strings already for col_name in self._string_columns: @@ -1102,7 +1122,7 @@ def _get_col_series_from_df(self, col_name, df): if col_name in self._primary_key: if len(self._primary_key) > 1: key_index = self._primary_key.index(col_name) - return df.index.get_level_values(level=key_index) + return df.index.levels[key_index] else: return df.index else: diff --git a/qgrid/tests/test_grid.py b/qgrid/tests/test_grid.py index cd4b1513..ee8af619 100644 --- a/qgrid/tests/test_grid.py +++ b/qgrid/tests/test_grid.py @@ -76,6 +76,7 @@ def test_edit_date(): def test_edit_multi_index_df(): df_multi = create_multi_index_df() + df_multi.index.set_names('first', level=0, inplace=True) view = QgridWidget(df=df_multi) old_val = df_multi.loc[('bar', 'two'), 1] @@ -493,7 +494,7 @@ def __init__(self, obj): def test_object_dtype(): - df = pd.DataFrame({'a': my_object_vals}) + df = pd.DataFrame({'a': my_object_vals}, index=my_object_vals) widget = QgridWidget(df=df) grid_data = json.loads(widget._df_json)['data'] @@ -520,6 +521,20 @@ def test_object_dtype(): assert not isinstance(grid_data[0]['a'], dict) assert not isinstance(grid_data[1]['a'], dict) + assert not isinstance(grid_data[0]['index'], dict) + assert not isinstance(grid_data[1]['index'], dict) + + +def test_index_categorical(): + df = pd.DataFrame({'foo': np.random.randn(3), 'future_index': [22, 13, 87]}) + df['future_index'] = df['future_index'].astype('category') + df = df.set_index('future_index') + widget = QgridWidget(df=df) + grid_data = json.loads(widget._df_json)['data'] + + assert not isinstance(grid_data[0]['future_index'], dict) + assert not isinstance(grid_data[1]['future_index'], dict) + def test_object_dtype_categorical(): cat_series = pd.Series( From caaa0cc8f071d474e4c266bb600851eaa2d09b2b Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Mon, 11 Jun 2018 02:49:04 -0400 Subject: [PATCH 2/9] Set the name of the series to whatever is set as the name of the series it's replacing (to avoid setting a name on a series that doesn't have one to start). --- qgrid/grid.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qgrid/grid.py b/qgrid/grid.py index c59902be..8e184e5e 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -1130,11 +1130,12 @@ def _get_col_series_from_df(self, col_name, df): def _set_col_series_on_df(self, col_name, df, col_series): if col_name in self._primary_key: - col_series.name = col_name if len(self._primary_key) > 1: key_index = self._primary_key.index(col_name) + col_series.name = df.index.levels[key_index].name df.index.set_levels(col_series, level=key_index, inplace=True) else: + col_series.name = df.index.name df.set_index(col_series, inplace=True) else: df[col_name] = col_series From aa777643d87be2fc33e13c8f4a30133f76bdfdb1 Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Tue, 12 Jun 2018 05:54:23 -0400 Subject: [PATCH 3/9] New "grouped" style for Dataframes with a multi-index. Don't show column name for unnamed index columns. --- js/src/qgrid.css | 48 ++++++++++++++++++++++++++- js/src/qgrid.widget.js | 10 ++++-- qgrid/grid.py | 71 +++++++++++++++++++++++++++++++++++++--- qgrid/tests/test_grid.py | 15 +++++++++ 4 files changed, 136 insertions(+), 8 deletions(-) diff --git a/js/src/qgrid.css b/js/src/qgrid.css index b7234d36..fe89bd19 100644 --- a/js/src/qgrid.css +++ b/js/src/qgrid.css @@ -322,11 +322,56 @@ .q-grid .slick-cell.idx-col { font-weight: bold; + border-right: 1px solid rgb(225, 232, 237); + margin-right: 3px; +} + +.q-grid .idx-col.group-top { + border-bottom-color: transparent; + background-color: #FFF; +} + +.q-grid .idx-col.group-middle { + border-top-color: transparent; + border-bottom-color: transparent; + color: transparent; + background-color: #FFF; +} + +.q-grid .idx-col.group-bottom { + border-top-color: transparent; + color: transparent; + background-color: #FFF; +} + +.q-grid .idx-col.group-single { + background-color: transparent; } .q-grid .slick-cell.l0.r0 { border-left: none; padding-left: 6px; + z-index: 90; +} + +.q-grid .slick-cell.l1.r1 { + z-index: 89; +} + +.q-grid .slick-cell.l2.r2 { + z-index: 88; +} + +.q-grid .slick-cell.l3.r3 { + z-index: 87; +} + +.q-grid .slick-cell.l4.r4 { + z-index: 86; +} + +.q-grid .slick-cell.l5.r5 { + z-index: 85; } .q-grid .slick-cell.selected { @@ -641,5 +686,6 @@ input.bool-filter-radio { } .slick-row .slick-cell:not(:first-child) { - padding-left: 4px; + padding-left: 5px; + margin-left: -4px; } diff --git a/js/src/qgrid.widget.js b/js/src/qgrid.widget.js index 06bafa21..b82beaba 100644 --- a/js/src/qgrid.widget.js +++ b/js/src/qgrid.widget.js @@ -204,6 +204,7 @@ class QgridView extends widgets.DOMWidgetView { this.data_view = this.create_data_view(df_json.data); this.grid_options = this.model.get('grid_options'); this.index_col_name = this.model.get("_index_col_name"); + this.row_styles = this.model.get("_row_styles"); this.columns = []; this.index_columns = []; @@ -346,6 +347,8 @@ class QgridView extends widgets.DOMWidgetView { if (cur_column.is_index) { slick_column.editor = editors.IndexEditor; slick_column.cssClass += ' idx-col'; + slick_column.name = cur_column.index_display_text; + slick_column.level = cur_column.level; this.index_columns.push(slick_column); continue; } @@ -386,6 +389,7 @@ class QgridView extends widgets.DOMWidgetView { }, 1); this.slick_grid.setSelectionModel(new Slick.RowSelectionModel()); + this.slick_grid.setCellCssStyles("grouping", this.row_styles); this.slick_grid.render(); var render_header_cell = (e, args) => { @@ -469,7 +473,7 @@ class QgridView extends widgets.DOMWidgetView { }; this.send(msg); this.viewport_timeout = null; - }, 100); + }, 10); }); // set up callbacks @@ -631,6 +635,7 @@ class QgridView extends widgets.DOMWidgetView { } this.update_timeout = setTimeout(() => { var df_json = JSON.parse(this.model.get('_df_json')); + this.row_styles = this.model.get("_row_styles"); var data_view = this.create_data_view(df_json.data); if (msg.triggered_by == 'sort_changed' && this.sort_indicator){ @@ -649,6 +654,7 @@ class QgridView extends widgets.DOMWidgetView { } this.set_data_view(data_view); + this.slick_grid.setCellCssStyles("grouping", this.row_styles); this.slick_grid.render(); if ((msg.triggered_by == 'add_row' || @@ -674,7 +680,7 @@ class QgridView extends widgets.DOMWidgetView { 'rows': selected_rows, 'type': 'selection_changed' }); - }, 100); + }, 10); } 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 8e184e5e..ce2f6309 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -470,6 +470,9 @@ class QgridWidget(widgets.DOMWidget): _df = Instance(pd.DataFrame) _df_json = Unicode('', sync=True) _primary_key = List() + _primary_key_display = Dict({}) + _row_styles = Dict({}, sync=True) + _disable_grouping = Bool(False) _columns = Dict({}, sync=True) _filter_tables = Dict({}) _sorted_column_cache = Dict({}) @@ -762,8 +765,10 @@ def should_be_stringified(col_series): for idx, cur_level in enumerate(df.index.levels): if cur_level.name: col_name = cur_level.name + self._primary_key_display[col_name] = col_name else: col_name = 'level_%s' % idx + self._primary_key_display[col_name] = "" self._primary_key.append(col_name) if should_be_stringified(cur_level): self._string_columns.append(col_name) @@ -771,8 +776,10 @@ def should_be_stringified(col_series): self._multi_index = False if df.index.name: col_name = df.index.name + self._primary_key_display[col_name] = col_name else: col_name = 'index' + self._primary_key_display[col_name] = "" self._primary_key = [col_name] if should_be_stringified(df.index): @@ -786,10 +793,50 @@ def should_be_stringified(col_series): series_to_set = df[sort_column_name] else: series_to_set = self._get_col_series_from_df( - col_name, df + col_name, df, level_vals=True ).map(str) self._set_col_series_on_df(col_name, df, series_to_set) + if type(df.index) == pd.core.index.MultiIndex and \ + not self._disable_grouping: + previous_value = None + row_styles = {} + row_styles_idx = 0 + for index, row in df.iterrows(): + row_style = {} + row_loc = self._df.index.get_loc(index) + last_row = row_loc == (len(self._df) - 1) + prev_idx = row_loc - 1 + for idx, index_val in enumerate(index): + col_name = self._primary_key[idx] + if previous_value == None: + row_style[col_name] = 'group-top' + continue + elif index_val == previous_value[idx]: + if prev_idx < 0: + row_style[col_name] = 'group-top' + continue + if row_styles[prev_idx][col_name] == 'group-top': + row_style[col_name] = 'group-middle' + elif row_styles[prev_idx][col_name] == 'group-bottom': + row_style[col_name] = 'group-top' + else: + row_style[col_name] = 'group-middle' + else: + row_style[col_name] = 'single' if last_row else 'group-top' + if prev_idx >= 0: + if row_styles[prev_idx][col_name] == 'group-middle': + row_styles[prev_idx][col_name] = 'group-bottom' + elif row_styles[prev_idx][col_name] == 'group-top': + row_styles[prev_idx][col_name] = 'group-single' + previous_value = index + row_styles[row_loc] = row_style + row_styles_idx += 1 + + self._row_styles = row_styles + else: + self._row_styles = {} + df_json = pd_json.to_json(None, df, orient='table', date_format='iso', @@ -825,6 +872,10 @@ def should_be_stringified(col_series): if col_name in self._primary_key: cur_column['is_index'] = True + cur_column['index_display_text'] = \ + self._primary_key_display[col_name] + if len(self._primary_key) > 0: + cur_column['level'] = self._primary_key.index(col_name) cur_column['position'] = i columns[col_name] = cur_column @@ -836,7 +887,9 @@ def should_be_stringified(col_series): # json that has interval columns replaced with text columns if len(self._interval_columns) > 0: for col_name in self._interval_columns: - col_series = self._get_col_series_from_df(col_name, df) + col_series = self._get_col_series_from_df(col_name, + df, + level_vals=True) col_series_as_strings = col_series.map(lambda x: str(x)) self._set_col_series_on_df(col_name, df, col_series_as_strings) @@ -850,7 +903,7 @@ def should_be_stringified(col_series): series_to_set = df[sort_column_name] else: series_to_set = self._get_col_series_from_df( - col_name, df + col_name, df, level_vals=True ).to_timestamp() self._set_col_series_on_df(col_name, df, series_to_set) @@ -882,6 +935,7 @@ def _update_sort(self): try: if self._sort_field is None: return + self._disable_grouping = False if self._sort_field in self._primary_key: if len(self._primary_key) == 1: self._df.sort_index( @@ -890,6 +944,7 @@ def _update_sort(self): ) else: level_id = self._sort_field + level_index = self._primary_key.index(level_id) if self._sort_field.startswith('level_'): level_id = int(self._sort_field[6:]) self._df.sort_index( @@ -897,12 +952,15 @@ def _update_sort(self): ascending=self._sort_ascending, inplace=True ) + if level_index > 0: + self._disable_grouping = True else: self._df.sort_values( self._sort_field, ascending=self._sort_ascending, inplace=True ) + self._disable_grouping = True except TypeError: self.log.info('TypeError occurred, assuming mixed data type ' 'column') @@ -1114,7 +1172,7 @@ def get_value_from_filter_table(k): }) # get any column from a dataframe, including index columns - def _get_col_series_from_df(self, col_name, df): + def _get_col_series_from_df(self, col_name, df, level_vals=False): sort_column_name = self._sort_helper_columns.get(col_name) if sort_column_name: return df[sort_column_name] @@ -1122,7 +1180,10 @@ def _get_col_series_from_df(self, col_name, df): if col_name in self._primary_key: if len(self._primary_key) > 1: key_index = self._primary_key.index(col_name) - return df.index.levels[key_index] + if level_vals: + return df.index.levels[key_index] + + return df.index.get_level_values(key_index) else: return df.index else: diff --git a/qgrid/tests/test_grid.py b/qgrid/tests/test_grid.py index ee8af619..5fbc4c57 100644 --- a/qgrid/tests/test_grid.py +++ b/qgrid/tests/test_grid.py @@ -382,6 +382,17 @@ def test_multi_index(): } }) + widget._handle_qgrid_msg_helper({ + 'type': 'filter_changed', + 'field': 'level_1', + 'filter_info': { + 'field': 'level_1', + 'type': 'text', + 'selected': [0], + 'excluded': [] + } + }) + widget._handle_qgrid_msg_helper({ 'type': 'sort_changed', 'sort_field': 3, @@ -411,6 +422,10 @@ def test_multi_index(): 'name': 'filter_changed', 'column': 3 }, + { + 'name': 'filter_changed', + 'column': 'level_1' + }, { 'name': 'sort_changed', 'old': { From 75f49d03d34b993afc5c858e48be86714aa1390e Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Tue, 12 Jun 2018 07:51:42 -0400 Subject: [PATCH 4/9] Python 2 support for unicode strings in columns, fix gaps in cell borders. --- js/src/qgrid.css | 25 +++++++++++++++++-------- js/src/qgrid.widget.js | 7 +++++++ qgrid/grid.py | 38 ++++++++++++++++++++++++++++++-------- qgrid/tests/test_grid.py | 5 ++++- 4 files changed, 58 insertions(+), 17 deletions(-) diff --git a/js/src/qgrid.css b/js/src/qgrid.css index fe89bd19..92333bd2 100644 --- a/js/src/qgrid.css +++ b/js/src/qgrid.css @@ -286,12 +286,12 @@ .q-grid .slick-cell { border-bottom: 1px solid #e1e8ed; border-right: none; - border-top: 1px solid transparent; border-left: 1px solid transparent; font-size: 13px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; - padding-top: 3px; padding-left: 0px; + padding-top: 4px; + border-top: none; } .q-grid.highlight-selected-row .slick-cell.selected { @@ -322,7 +322,16 @@ .q-grid .slick-cell.idx-col { font-weight: bold; + margin-right: 3px; +} + +.q-grid .slick-cell.idx-col.last-idx-col { border-right: 1px solid rgb(225, 232, 237); +} + +.q-grid .slick-cell.idx-col:not(.first-idx-col) { + font-weight: bold; + border-left: 1px solid rgb(225, 232, 237); margin-right: 3px; } @@ -351,27 +360,27 @@ .q-grid .slick-cell.l0.r0 { border-left: none; padding-left: 6px; - z-index: 90; + z-index: 85; } .q-grid .slick-cell.l1.r1 { - z-index: 89; + z-index: 86; } .q-grid .slick-cell.l2.r2 { - z-index: 88; + z-index: 87; } .q-grid .slick-cell.l3.r3 { - z-index: 87; + z-index: 88; } .q-grid .slick-cell.l4.r4 { - z-index: 86; + z-index: 89; } .q-grid .slick-cell.l5.r5 { - z-index: 85; + z-index: 90; } .q-grid .slick-cell.selected { diff --git a/js/src/qgrid.widget.js b/js/src/qgrid.widget.js index b82beaba..5a8c6c4a 100644 --- a/js/src/qgrid.widget.js +++ b/js/src/qgrid.widget.js @@ -347,6 +347,13 @@ class QgridView extends widgets.DOMWidgetView { if (cur_column.is_index) { slick_column.editor = editors.IndexEditor; slick_column.cssClass += ' idx-col'; + if (cur_column.first_index){ + slick_column.cssClass += ' first-idx-col'; + } + if (cur_column.last_index){ + slick_column.cssClass += ' last-idx-col'; + } + slick_column.name = cur_column.index_display_text; slick_column.level = cur_column.level; this.index_columns.push(slick_column); diff --git a/qgrid/grid.py b/qgrid/grid.py index ce2f6309..1fed42c3 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -19,6 +19,7 @@ ) from itertools import chain from uuid import uuid4 +from six import string_types # 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 @@ -354,6 +355,13 @@ def show_grid(data_frame, show_toolbar=None, PAGE_SIZE = 100 +def stringify(x): + if isinstance(x, string_types): + return x + else: + return str(x) + + @widgets.register() class QgridWidget(widgets.DOMWidget): """ @@ -794,7 +802,7 @@ def should_be_stringified(col_series): else: series_to_set = self._get_col_series_from_df( col_name, df, level_vals=True - ).map(str) + ).map(stringify) self._set_col_series_on_df(col_name, df, series_to_set) if type(df.index) == pd.core.index.MultiIndex and \ @@ -809,7 +817,7 @@ def should_be_stringified(col_series): prev_idx = row_loc - 1 for idx, index_val in enumerate(index): col_name = self._primary_key[idx] - if previous_value == None: + if previous_value is None: row_style[col_name] = 'group-top' continue elif index_val == previous_value[idx]: @@ -823,11 +831,16 @@ def should_be_stringified(col_series): else: row_style[col_name] = 'group-middle' else: - row_style[col_name] = 'single' if last_row else 'group-top' + if last_row: + row_style[col_name] = 'single' + else: + row_style[col_name] = 'group-top' if prev_idx >= 0: - if row_styles[prev_idx][col_name] == 'group-middle': + if row_styles[prev_idx][col_name] == \ + 'group-middle': row_styles[prev_idx][col_name] = 'group-bottom' - elif row_styles[prev_idx][col_name] == 'group-top': + elif row_styles[prev_idx][col_name] == \ + 'group-top': row_styles[prev_idx][col_name] = 'group-single' previous_value = index row_styles[row_loc] = row_style @@ -855,9 +868,13 @@ def should_be_stringified(col_series): if ('primaryKey' in df_schema): self._primary_key = df_schema['primaryKey'] else: - # for some reason, 'primaryKey' isn't set when the index is - # a single interval column. that's why this case is here. - self._primary_key = [df.index.name] + # for some reason, 'primaryKey' isn't set in certain cases, + # like when we have an interval index. that's why this case + # is here. + if df.index.name is not None: + self._primary_key = [df.index.name] + else: + self._primary_key = ['index'] columns = {} for i, cur_column in enumerate(df_schema['fields']): @@ -876,6 +893,11 @@ def should_be_stringified(col_series): self._primary_key_display[col_name] if len(self._primary_key) > 0: cur_column['level'] = self._primary_key.index(col_name) + level = self._primary_key.index(col_name) + if level == 0: + cur_column['first_index'] = True + if level == (len(self._primary_key) - 1): + cur_column['last_index'] = True cur_column['position'] = i columns[col_name] = cur_column diff --git a/qgrid/tests/test_grid.py b/qgrid/tests/test_grid.py index 5fbc4c57..2feddd02 100644 --- a/qgrid/tests/test_grid.py +++ b/qgrid/tests/test_grid.py @@ -541,7 +541,10 @@ def test_object_dtype(): def test_index_categorical(): - df = pd.DataFrame({'foo': np.random.randn(3), 'future_index': [22, 13, 87]}) + df = pd.DataFrame({ + 'foo': np.random.randn(3), + 'future_index': [22, 13, 87] + }) df['future_index'] = df['future_index'].astype('category') df = df.set_index('future_index') widget = QgridWidget(df=df) From b105034b92ca60a87630710e5aefe63504aa951d Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Tue, 12 Jun 2018 12:28:45 -0400 Subject: [PATCH 5/9] Disable grouping when we're filtered by any other column other than level 0 of the multiindex. Fix for converting PeriodIndex values to strings in pandas 0.18.x. Don't get the _primary_key from the generated json anymore, because it seems it's often not included and we already have code that figures it out. --- js/package.json | 2 +- js/src/qgrid.widget.js | 23 ++++++++++++++++++++--- qgrid/_version.py | 2 +- qgrid/grid.py | 40 ++++++++++++++-------------------------- 4 files changed, 36 insertions(+), 31 deletions(-) diff --git a/js/package.json b/js/package.json index 195c7dea..fb4d0ab0 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "qgrid", - "version": "1.0.5", + "version": "1.0.6-beta.3", "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 5a8c6c4a..f79e088b 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.5', - _view_module_version : '^1.0.5', + _model_module_version : '^1.0.6-beta.3', + _view_module_version : '^1.0.6-beta.3', _df_json: '', _columns: {} }); @@ -643,6 +643,7 @@ class QgridView extends widgets.DOMWidgetView { this.update_timeout = setTimeout(() => { var df_json = JSON.parse(this.model.get('_df_json')); this.row_styles = this.model.get("_row_styles"); + this.multi_index = this.model.get("_multi_index"); var data_view = this.create_data_view(df_json.data); if (msg.triggered_by == 'sort_changed' && this.sort_indicator){ @@ -661,7 +662,23 @@ class QgridView extends widgets.DOMWidgetView { } this.set_data_view(data_view); - this.slick_grid.setCellCssStyles("grouping", this.row_styles); + + var skip_grouping = false; + if (this.multi_index) { + for (var i=1; i < this.filter_list.length; i++) { + var cur_filter = this.filter_list[i]; + if (cur_filter.is_active()) { + skip_grouping = true; + } + } + } + + if (skip_grouping) { + this.slick_grid.removeCellCssStyles("grouping"); + } else { + this.slick_grid.setCellCssStyles("grouping", this.row_styles); + } + this.slick_grid.render(); if ((msg.triggered_by == 'add_row' || diff --git a/qgrid/_version.py b/qgrid/_version.py index 50954c41..d6589238 100644 --- a/qgrid/_version.py +++ b/qgrid/_version.py @@ -1,4 +1,4 @@ -version_info = (1, 0, 5, 'final') +version_info = (1, 0, 6, 'beta', 3) _specifier_ = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc', 'final': ''} diff --git a/qgrid/grid.py b/qgrid/grid.py index 1fed42c3..cd6432c1 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -472,8 +472,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.5').tag(sync=True) - _model_module_version = Unicode('1.0.5').tag(sync=True) + _view_module_version = Unicode('1.0.6-beta.3').tag(sync=True) + _model_module_version = Unicode('1.0.6-beta.3').tag(sync=True) _df = Instance(pd.DataFrame) _df_json = Unicode('', sync=True) @@ -493,7 +493,7 @@ class QgridWidget(widgets.DOMWidget): _unfiltered_df = Instance(pd.DataFrame) _index_col_name = Unicode('qgrid_unfiltered_index', sync=True) _sort_col_suffix = Unicode('_qgrid_sort_column') - _multi_index = Bool(False) + _multi_index = Bool(False, sync=True) _edited = Bool(False) _selected_rows = List([]) _viewport_range = Tuple(Integer(), Integer(), default_value=(0, 100)) @@ -766,7 +766,8 @@ def _update_table(self, def should_be_stringified(col_series): return col_series.dtype == np.dtype('O') or \ - hasattr(col_series, 'cat') + hasattr(col_series, 'cat') or \ + isinstance(col_series, pd.PeriodIndex) if type(df.index) == pd.core.index.MultiIndex: self._multi_index = True @@ -809,10 +810,9 @@ def should_be_stringified(col_series): not self._disable_grouping: previous_value = None row_styles = {} - row_styles_idx = 0 + row_loc = from_index for index, row in df.iterrows(): row_style = {} - row_loc = self._df.index.get_loc(index) last_row = row_loc == (len(self._df) - 1) prev_idx = row_loc - 1 for idx, index_val in enumerate(index): @@ -844,7 +844,7 @@ def should_be_stringified(col_series): row_styles[prev_idx][col_name] = 'group-single' previous_value = index row_styles[row_loc] = row_style - row_styles_idx += 1 + row_loc += 1 self._row_styles = row_styles else: @@ -865,17 +865,6 @@ def should_be_stringified(col_series): parsed_json = json.loads(df_json) df_schema = parsed_json['schema'] - if ('primaryKey' in df_schema): - self._primary_key = df_schema['primaryKey'] - else: - # for some reason, 'primaryKey' isn't set in certain cases, - # like when we have an interval index. that's why this case - # is here. - if df.index.name is not None: - self._primary_key = [df.index.name] - else: - self._primary_key = ['index'] - columns = {} for i, cur_column in enumerate(df_schema['fields']): col_name = cur_column['name'] @@ -896,7 +885,7 @@ def should_be_stringified(col_series): level = self._primary_key.index(col_name) if level == 0: cur_column['first_index'] = True - if level == (len(self._primary_key) - 1): + if self._multi_index and level == (len(self._primary_key) - 1): cur_column['last_index'] = True cur_column['position'] = i @@ -965,12 +954,9 @@ def _update_sort(self): inplace=True ) else: - level_id = self._sort_field - level_index = self._primary_key.index(level_id) - if self._sort_field.startswith('level_'): - level_id = int(self._sort_field[6:]) + level_index = self._primary_key.index(self._sort_field) self._df.sort_index( - level=level_id, + level=level_index, ascending=self._sort_ascending, inplace=True ) @@ -1215,11 +1201,13 @@ def _set_col_series_on_df(self, col_name, df, col_series): if col_name in self._primary_key: if len(self._primary_key) > 1: key_index = self._primary_key.index(col_name) - col_series.name = df.index.levels[key_index].name + prev_name = df.index.levels[key_index].name df.index.set_levels(col_series, level=key_index, inplace=True) + df.index.rename(prev_name, level=key_index, inplace=True) else: - col_series.name = df.index.name + prev_name = df.index.name df.set_index(col_series, inplace=True) + df.index.rename(prev_name) else: df[col_name] = col_series From 711790af7dfa6889cc2738655482a267e5d1b479 Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Tue, 12 Jun 2018 21:41:05 -0400 Subject: [PATCH 6/9] Fix issue where the border for the edit cell box was getting cut off. --- js/src/qgrid.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/js/src/qgrid.css b/js/src/qgrid.css index 92333bd2..a8e1dc42 100644 --- a/js/src/qgrid.css +++ b/js/src/qgrid.css @@ -383,6 +383,10 @@ z-index: 90; } +.q-grid .slick-cell.editable { + z-index: 91 !important; +} + .q-grid .slick-cell.selected { background-color: transparent; } From 15a496a8aabbd4efb84918e02da57d48b2c8ec4f Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Wed, 13 Jun 2018 12:44:51 -0400 Subject: [PATCH 7/9] Fix issue where the date control's next/prev icons weren't appearing. Also fixed an issue where the selected date filter wouldn't be restored when the date filter was reopened. --- js/src/qgrid.css | 22 +++++++++++++++++++++- js/src/qgrid.datefilter.js | 30 ++++++++++++++++++++++-------- js/src/qgrid.widget.js | 4 +++- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/js/src/qgrid.css b/js/src/qgrid.css index a8e1dc42..d215ced8 100644 --- a/js/src/qgrid.css +++ b/js/src/qgrid.css @@ -635,7 +635,7 @@ z-index: 9999 !important; } -#ui-datepicker-div .ui-datepicker { +#ui-datepicker-div.ui-datepicker { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; /*@include box-shadow(0 5px 10px rgba(0, 0, 0, 0.2))*/ z-index: 99 !important; @@ -644,6 +644,26 @@ border: 1px solid gray; } +#ui-datepicker-div.ui-datepicker .ui-icon { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-indent: 0px; + margin-left: -4px; + margin-top: -6px; +} + +#ui-datepicker-div.ui-datepicker .ui-icon-circle-triangle-w:before { + content: "\f053"; +} + +#ui-datepicker-div.ui-datepicker .ui-icon-circle-triangle-e:before { + content: "\f054"; +} + /* from slickgrid editing example */ input.editor-text { diff --git a/js/src/qgrid.datefilter.js b/js/src/qgrid.datefilter.js index dc49b780..a4cf8acd 100644 --- a/js/src/qgrid.datefilter.js +++ b/js/src/qgrid.datefilter.js @@ -83,14 +83,14 @@ class DateFilter extends filter_base.FilterBase { var end_date_string = this.end_date_control.val(); var start_date = new Date(start_date_string); + var end_date = new Date(end_date_string); - // use the last millisecond of the end_date (1000ms * 60s * 60m * 24h) - var end_date = new Date( - (new Date(end_date_string).getTime()) + (1000 * 60 * 60 * 24) - 1 - ); + start_date = Date.UTC(start_date.getUTCFullYear(), start_date.getUTCMonth(), start_date.getUTCDate()); + end_date = Date.UTC(end_date.getUTCFullYear(), end_date.getUTCMonth(), end_date.getUTCDate()); + end_date += (1000 * 60 * 60 * 24) - 1; - this.filter_start_date = start_date.getTime(); - this.filter_end_date = end_date.getTime(); + this.filter_start_date = start_date; + this.filter_end_date = end_date; this.send_filter_changed(); @@ -112,8 +112,22 @@ class DateFilter extends filter_base.FilterBase { this.filter_elem.find(".datepicker").datepicker(date_options); - this.start_date_control.datepicker("setDate", this.min_date); - this.end_date_control.datepicker("setDate", this.max_date); + if (this.filter_start_date != null){ + this.start_date_control.datepicker("setDate", this.get_utc_date(this.filter_start_date)); + } else { + this.start_date_control.datepicker("setDate", this.min_date); + } + + if (this.filter_end_date != null){ + this.end_date_control.datepicker("setDate", this.get_utc_date(this.filter_end_date)); + } else { + this.end_date_control.datepicker("setDate", this.max_date); + } + } + + get_utc_date(date_ms) { + var date = new Date(date_ms); + return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()); } get_filter_info() { diff --git a/js/src/qgrid.widget.js b/js/src/qgrid.widget.js index f79e088b..51613738 100644 --- a/js/src/qgrid.widget.js +++ b/js/src/qgrid.widget.js @@ -302,7 +302,9 @@ class QgridView extends widgets.DOMWidgetView { $.datepicker.setDefaults({ gotoCurrent: true, dateFormat: $.datepicker.ISO_8601, - constrainInput: false + constrainInput: false, + "prevText": "", + "nextText": "" }); var sorted_columns = Object.values(columns).sort( From 54535be86063803ae3b1f25cb404ebb6779d8483 Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Wed, 13 Jun 2018 13:04:59 -0400 Subject: [PATCH 8/9] Some css changes to avoid js console errors about not being able to load various images. --- js/src/qgrid.css | 4 +++- qgrid/tests/test_grid.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/js/src/qgrid.css b/js/src/qgrid.css index d215ced8..0afc86d0 100644 --- a/js/src/qgrid.css +++ b/js/src/qgrid.css @@ -186,7 +186,8 @@ border-right: none; } -.q-grid .slick-header-columns { +.q-grid .slick-header-columns, +.text-filter-grid .slick-header-columns { background: none; background-color: rgb(245, 245, 245); border-bottom: none; @@ -654,6 +655,7 @@ text-indent: 0px; margin-left: -4px; margin-top: -6px; + background-image: none; } #ui-datepicker-div.ui-datepicker .ui-icon-circle-triangle-w:before { diff --git a/qgrid/tests/test_grid.py b/qgrid/tests/test_grid.py index 2feddd02..ca0f8f3e 100644 --- a/qgrid/tests/test_grid.py +++ b/qgrid/tests/test_grid.py @@ -152,7 +152,7 @@ def test_add_row(): # expected values added_index = event_history[0]['index'] expected_values = np.array( - [4, 1.0, 1.0, 3, pd.Timestamp('2013-01-02 00:00:00'), 'bar', 'fox'], + [4, 1.0, pd.Timestamp('2013-01-02 00:00:00'), 1.0, 3, 'bar', 'fox'], dtype=object ) assert (widget._df.loc[added_index].values == expected_values).all() From 5b9ffe0ce4ddc7e01c40d0bdaad14cfe968166a9 Mon Sep 17 00:00:00 2001 From: Tim Shawver Date: Thu, 14 Jun 2018 10:10:26 -0400 Subject: [PATCH 9/9] Bump to 1.0.6 beta 6. --- 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 fb4d0ab0..690b9416 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "qgrid", - "version": "1.0.6-beta.3", + "version": "1.0.6-beta.6", "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 51613738..a4417522 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.6-beta.3', - _view_module_version : '^1.0.6-beta.3', + _model_module_version : '^1.0.6-beta.6', + _view_module_version : '^1.0.6-beta.6', _df_json: '', _columns: {} }); diff --git a/qgrid/_version.py b/qgrid/_version.py index d6589238..d1a1301c 100644 --- a/qgrid/_version.py +++ b/qgrid/_version.py @@ -1,4 +1,4 @@ -version_info = (1, 0, 6, 'beta', 3) +version_info = (1, 0, 6, 'beta', 6) _specifier_ = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc', 'final': ''} diff --git a/qgrid/grid.py b/qgrid/grid.py index cd6432c1..e3e8f360 100644 --- a/qgrid/grid.py +++ b/qgrid/grid.py @@ -472,8 +472,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.6-beta.3').tag(sync=True) - _model_module_version = Unicode('1.0.6-beta.3').tag(sync=True) + _view_module_version = Unicode('1.0.6-beta.6').tag(sync=True) + _model_module_version = Unicode('1.0.6-beta.6').tag(sync=True) _df = Instance(pd.DataFrame) _df_json = Unicode('', sync=True)