diff --git a/examples/DataGrid.ipynb b/examples/DataGrid.ipynb index 90bf6b6d..97a78595 100644 --- a/examples/DataGrid.ipynb +++ b/examples/DataGrid.ipynb @@ -156,10 +156,10 @@ " {\n", " \"type\": \"filter\",\n", " \"operator\": \"=\",\n", - " \"columnIndex\": 7,\n", + " \"column\": \"Origin\",\n", " \"value\": \"Europe\",\n", " },\n", - " {\"type\": \"sort\", \"columnIndex\": 3, \"desc\": True},\n", + " {\"type\": \"sort\", \"column\": \"Horsepower\", \"desc\": True},\n", " ]\n", ")" ] @@ -181,9 +181,9 @@ "source": [ "datagrid.transform(\n", " [\n", - " {\"type\": \"filter\", \"operator\": \"=\", \"columnIndex\": 7, \"value\": \"USA\"},\n", - " {\"type\": \"filter\", \"operator\": \"<\", \"columnIndex\": 1, \"value\": 13},\n", - " {\"type\": \"sort\", \"columnIndex\": 1},\n", + " {\"type\": \"filter\", \"operator\": \"=\", \"column\": \"Origin\", \"value\": \"USA\"},\n", + " {\"type\": \"filter\", \"operator\": \"<\", \"column\": \"Horsepower\", \"value\": 130},\n", + " {\"type\": \"sort\", \"column\": \"Acceleration\"},\n", " ]\n", ")" ] @@ -372,7 +372,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.1" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/examples/Nested Hierarchies.ipynb b/examples/Nested Hierarchies.ipynb index 2934e281..92b8eaf4 100644 --- a/examples/Nested Hierarchies.ipynb +++ b/examples/Nested Hierarchies.ipynb @@ -64,11 +64,19 @@ "\n", "nested_grid" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23f007f4-bcce-4ecd-b68c-aa9b96c82f82", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -82,7 +90,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.5" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/examples/Pandas.ipynb b/examples/Pandas.ipynb index 8b1ef303..57d073f7 100644 --- a/examples/Pandas.ipynb +++ b/examples/Pandas.ipynb @@ -55,7 +55,7 @@ "}\n", "\n", "grid = DataGrid(df, base_row_size=30, base_column_size=300, renderers=renderers)\n", - "grid.transform([{\"type\": \"sort\", \"columnIndex\": 2, \"desc\": True}])\n", + "grid.transform([{\"type\": \"sort\", \"column\": \"Dates\", \"desc\": True}])\n", "grid" ] }, @@ -69,7 +69,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -83,7 +83,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.6" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/examples/Trees_paris.ipynb b/examples/Trees_paris.ipynb index ab023b70..4c17d8fb 100644 --- a/examples/Trees_paris.ipynb +++ b/examples/Trees_paris.ipynb @@ -130,28 +130,21 @@ "source": [ "datagrid.transform(\n", " [\n", - " {\"type\": \"sort\", \"columnIndex\": 5},\n", + " {\"type\": \"sort\", \"column\": \"Height_m\"},\n", " {\n", " \"type\": \"filter\",\n", " \"operator\": \"=\",\n", - " \"columnIndex\": 4,\n", - " \"value\": \"Platane\",\n", + " \"column\": \"Kind\",\n", + " \"value\": \"Platanus\",\n", " },\n", " ]\n", ")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -165,7 +158,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.6" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/ipydatagrid/datagrid.py b/ipydatagrid/datagrid.py index 623bc6d6..910f4ca8 100644 --- a/ipydatagrid/datagrid.py +++ b/ipydatagrid/datagrid.py @@ -3,6 +3,7 @@ import datetime import decimal +import warnings from collections.abc import Iterator from copy import deepcopy from math import floor @@ -825,8 +826,22 @@ def _validate_transforms(self, proposal): transforms = proposal["value"] field_len = len(self._data["schema"]["fields"]) for transform in transforms: - if transform["columnIndex"] > field_len: - raise ValueError("Column index is out of bounds.") + if "columnIndex" in transform: + warnings.warn( + "Applying transforms on columnIndex is deprecated, " + "please provide the column name instead", + DeprecationWarning, + stacklevel=4, + ) + + if "column" not in transform: + transform["column"] = self._data["schema"]["fields"][ + transform["columnIndex"] + ]["name"] + + if transform["columnIndex"] > field_len: + raise ValueError("Column index is out of bounds.") + return transforms @validate("_data") diff --git a/js/core/filterMenu.ts b/js/core/filterMenu.ts index f504c619..5e4aa721 100644 --- a/js/core/filterMenu.ts +++ b/js/core/filterMenu.ts @@ -89,6 +89,10 @@ export class InteractiveFilterDialog extends BoxPanel { this.addWidget(this._applyWidget); } + get column(): string { + return this._column; + } + /** * Connects to the "Select All" widget signal and * toggles checking all/none of the unique elements @@ -151,15 +155,12 @@ export class InteractiveFilterDialog extends BoxPanel { const value = this._mode === 'condition' ? this._filterValue - : this._uniqueValueStateManager.getValues( - this.region, - this._columnIndex, - ); + : this._uniqueValueStateManager.getValues(this.region, this._column); // Construct transform const transform: Transform.TransformSpec = { type: 'filter', - columnIndex: this.model.getSchemaIndex(this._region, this._columnIndex), + column: this._column, operator: this._filterOperator, value: value, }; @@ -172,11 +173,7 @@ export class InteractiveFilterDialog extends BoxPanel { * Updates the DOM elements with transform state from the linked data model. */ updateDialog(): void { - const lookupColumn = this.model.getSchemaIndex( - this._region, - this._columnIndex, - ); - const columnState = this._model.transformMetadata(lookupColumn); + const columnState = this._model.transformMetadata(this._column); // Update state with transform metadata, if present if (columnState && columnState.filter) { @@ -254,10 +251,7 @@ export class InteractiveFilterDialog extends BoxPanel { * Displays the unique values of a column. */ _renderUniqueVals() { - const uniqueVals = this._model.uniqueValues( - this._region, - this._columnIndex, - ); + const uniqueVals = this._model.uniqueValues(this._region, this._column); const data = new DataSource( { index: [...uniqueVals.keys()], uniqueVals }, [{ index: null }, { uniqueVals: null }], @@ -273,6 +267,7 @@ export class InteractiveFilterDialog extends BoxPanel { this._uniqueValueGrid.dataModel = new ViewBasedJSONModel(data); const sortTransform: Transform.Sort = { type: 'sort', + column: 'uniqueVals', columnIndex: this.model.getSchemaIndex(this._region, 0), desc: false, }; @@ -295,21 +290,14 @@ export class InteractiveFilterDialog extends BoxPanel { return; } - const uniqueVals = this._model.uniqueValues( - this._region, - this._columnIndex, - ); + const uniqueVals = this._model.uniqueValues(this._region, this._column); let showAsChecked = true; for (const value of uniqueVals) { // If there is a unique value which is not present in the state then it is // not ticked, and therefore we should not tick the "Select all" checkbox. if ( - !this._uniqueValueStateManager.has( - this._region, - this._columnIndex, - value, - ) + !this._uniqueValueStateManager.has(this._region, this._column, value) ) { showAsChecked = false; break; @@ -325,20 +313,17 @@ export class InteractiveFilterDialog extends BoxPanel { */ open(options: InteractiveFilterDialog.IOpenOptions): void { // Update state with the metadata of the event that opened the menu. - this._columnIndex = options.columnIndex; + this._column = options.column; this._columnDType = this._model.metadata( options.region, 0, - options.columnIndex, + this._model.columnNameToIndex(this._column), )['type']; this._region = options.region; this._mode = options.mode; // Setting filter flag - this.hasFilter = - this._model.getFilterTransform( - this.model.getSchemaIndex(this._region, this._columnIndex), - ) !== undefined; + this.hasFilter = this._model.getFilterTransform(this._column) !== undefined; this.userInteractedWithDialog = false; @@ -524,8 +509,7 @@ export class InteractiveFilterDialog extends BoxPanel { const value = this._textInputFilterValue; const transform: Transform.TransformSpec = { type: 'filter', - // This is a separate data grid for the dialog box - // which will always have two columns. + column: this._column, columnIndex: 1, operator: 'stringContains', value: value, @@ -728,10 +712,7 @@ export class InteractiveFilterDialog extends BoxPanel { * values of a column. */ protected async createUniqueValueNodes(): Promise { - const uniqueVals = this._model.uniqueValues( - this._region, - this._columnIndex, - ); + const uniqueVals = this._model.uniqueValues(this._region, this._column); const optionElems = uniqueVals.map((val) => { return h.option({ value: val }, String(val)); }); @@ -1141,21 +1122,13 @@ export class InteractiveFilterDialog extends BoxPanel { } addRemoveAllUniqueValuesToState(add: boolean) { - const uniqueVals = this.model.uniqueValues(this._region, this._columnIndex); + const uniqueVals = this.model.uniqueValues(this._region, this._column); for (const value of uniqueVals) { if (add) { - this._uniqueValueStateManager.add( - this._region, - this._columnIndex, - value, - ); + this._uniqueValueStateManager.add(this._region, this._column, value); } else { - this._uniqueValueStateManager.remove( - this._region, - this._columnIndex, - value, - ); + this._uniqueValueStateManager.remove(this._region, this._column, value); } } } @@ -1195,13 +1168,6 @@ export class InteractiveFilterDialog extends BoxPanel { return this._region; } - /** - * Returns the active Cellregion. - */ - get columnIndex(): number { - return this._columnIndex; - } - /** * Returns the active column dtype. */ @@ -1213,7 +1179,7 @@ export class InteractiveFilterDialog extends BoxPanel { // Cell metadata private _columnDType = 'number'; - private _columnIndex = 0; + private _column: string; private _region: DataModel.CellRegion = 'column-header'; // Menu state @@ -1417,6 +1383,12 @@ export namespace InteractiveFilterDialog { region: DataModel.CellRegion; /** + * The column of the `cellClick` that triggered this call. + */ + column: string; + + /** + * (deprecated) * The column index of the `cellClick` that triggered this call. */ columnIndex: number; @@ -1449,17 +1421,17 @@ export class UniqueValueStateManager { this._grid = options.grid; } - has(region: DataModel.CellRegion, columnIndex: number, value: any): boolean { - const key = this.getKeyName(region, columnIndex); + has(region: DataModel.CellRegion, column: string, value: any): boolean { + const key = this.getKeyName(region, column); return this._state.hasOwnProperty(key) && this._state[key].has(value); } - getKeyName(region: DataModel.CellRegion, columnIndex: number): string { - return `${region}:${columnIndex}`; + getKeyName(region: DataModel.CellRegion, column: string): string { + return `${region}:${column}`; } - add(region: DataModel.CellRegion, columnIndex: number, value: any): void { - const key = this.getKeyName(region, columnIndex); + add(region: DataModel.CellRegion, column: string, value: any): void { + const key = this.getKeyName(region, column); if (this._state.hasOwnProperty(key)) { this._state[key].add(value); } else { @@ -1470,8 +1442,8 @@ export class UniqueValueStateManager { MessageLoop.postMessage(this._grid.viewport, msg); } - remove(region: DataModel.CellRegion, columnIndex: number, value: any): void { - const key = this.getKeyName(region, columnIndex); + remove(region: DataModel.CellRegion, column: string, value: any): void { + const key = this.getKeyName(region, column); if (this._state.hasOwnProperty(key)) { this._state[key].delete(value); @@ -1480,8 +1452,8 @@ export class UniqueValueStateManager { MessageLoop.postMessage(this._grid.viewport, msg); } - getValues(region: DataModel.CellRegion, columnIndex: number): any[] { - const key = this.getKeyName(region, columnIndex); + getValues(region: DataModel.CellRegion, column: string): any[] { + const key = this.getKeyName(region, column); if (this._state.hasOwnProperty(key)) { return Array.from(this._state[key]); } else { @@ -1516,7 +1488,7 @@ class UniqueValueGridMouseHandler extends BasicMouseHandler { return; } const row = hit.row; - const colIndex = this._filterDialog.columnIndex; + const colIndex = this._filterDialog.column; const region = this._filterDialog.region; const value = grid.dataModel!.data('body', row, 0); diff --git a/js/core/gridContextMenu.ts b/js/core/gridContextMenu.ts index 3b0f6a11..0f44268f 100644 --- a/js/core/gridContextMenu.ts +++ b/js/core/gridContextMenu.ts @@ -212,7 +212,10 @@ export class FeatherGridContextMenu extends GridContextMenu { // Jupyter Lab, Notebook < 7, NbClassic and Voila. Until this is available in lumino/widgets, // detach and reattach the menu here. const bodyFirstChild = document.body.firstElementChild; - if (this._menu.node.parentElement == document.body && bodyFirstChild != this._menu.node) { + if ( + this._menu.node.parentElement == document.body && + bodyFirstChild != this._menu.node + ) { Widget.detach(this._menu); Widget.attach(this._menu, document.body, bodyFirstChild as HTMLElement); } diff --git a/js/core/headerRenderer.ts b/js/core/headerRenderer.ts index 34318458..e93ce4a1 100644 --- a/js/core/headerRenderer.ts +++ b/js/core/headerRenderer.ts @@ -290,8 +290,10 @@ export class HeaderRenderer extends TextRenderer { config.column, ); + const column = this.model.currentView.dataset.columns[schemaIndex]; + const colMetaData: TransformStateManager.IColumn | undefined = - this.model.transformMetadata(schemaIndex); + this.model.transformMetadata(column); // Fill filter icon if filter applied if (colMetaData && colMetaData['filter']) { diff --git a/js/core/transform.ts b/js/core/transform.ts index bebc3a15..23584691 100644 --- a/js/core/transform.ts +++ b/js/core/transform.ts @@ -16,7 +16,13 @@ export namespace Transform { /** * The column in the data schema to apply the transformation to. */ - columnIndex: number; + column: string; + + /** + * (deprecated) + * The column in the data schema to apply the transformation to. + */ + columnIndex?: number; /** * Indicates if the sort should be performed descending or ascending. @@ -36,7 +42,13 @@ export namespace Transform { /** * The column in the data schema to apply the transformation to. */ - columnIndex: number; + column: string; + + /** + * (deprecated) + * The column in the data schema to apply the transformation to. + */ + columnIndex?: number; /** * The operator for this transformation. diff --git a/js/core/transformStateManager.ts b/js/core/transformStateManager.ts index 297fcc90..6a6272f4 100644 --- a/js/core/transformStateManager.ts +++ b/js/core/transformStateManager.ts @@ -21,8 +21,8 @@ import { DataSource } from '../datasource'; export class TransformStateManager { protected _add(transform: Transform.TransformSpec): void { // Add column to state if not already present - if (!this._state.hasOwnProperty(transform.columnIndex)) { - this._state[transform.columnIndex] = { + if (!this._state.hasOwnProperty(transform.column)) { + this._state[transform.column] = { sort: undefined, filter: undefined, }; @@ -36,10 +36,10 @@ export class TransformStateManager { for (const key of Object.keys(this._state)) { this._state[key]['sort'] = undefined; } - this._state[transform.columnIndex]['sort'] = transform; + this._state[transform.column]['sort'] = transform; break; case 'filter': - this._state[transform.columnIndex]['filter'] = transform; + this._state[transform.column]['filter'] = transform; break; default: throw 'unreachable'; @@ -111,21 +111,36 @@ export class TransformStateManager { const sortExecutors: SortExecutor[] = []; const filterExecutors: FilterExecutor[] = []; - Object.keys(this._state).forEach((columnIndex) => { - const transform: TransformStateManager.IColumn = this._state[columnIndex]; + Object.keys(this._state).forEach((column) => { + const transform: TransformStateManager.IColumn = this._state[column]; if (transform.sort) { + let dType = ''; + for (const field of data.schema.fields) { + if (field.name === transform.sort.column) { + dType = field.type; + } + } + const executor = new SortExecutor({ - field: data.schema.fields[transform.sort.columnIndex]['name'], - dType: data.schema.fields[transform.sort.columnIndex]['type'], + field: transform.sort.column, + dType, desc: transform.sort.desc, }); sortExecutors.push(executor); } + if (transform.filter) { + let dType = ''; + for (const field of data.schema.fields) { + if (field.name === transform.filter.column) { + dType = field.type; + } + } + const executor = new FilterExecutor({ - field: data.schema.fields[transform.filter.columnIndex]['name'], - dType: data.schema.fields[transform.filter.columnIndex]['type'], + field: transform.filter.column, + dType, operator: transform.filter.operator, value: transform.filter.value, }); @@ -140,18 +155,18 @@ export class TransformStateManager { /** * Removes the provided transformation from the active state. * - * @param columnIndex - The index of the column state to be removed. + * @param column - The index of the column state to be removed. * * @param transformType - The type of the transform to be removed from state. */ - remove(columnIndex: number, transformType: string): void { + remove(column: string, transformType: string): void { // Return immediately if the key is not in the state - if (!this._state.hasOwnProperty(columnIndex)) { + if (!this._state.hasOwnProperty(column)) { return; } try { - const columnState = this._state[columnIndex]; + const columnState = this._state[column]; if (transformType === 'sort') { columnState.sort = undefined; } else if (transformType === 'filter') { @@ -160,7 +175,7 @@ export class TransformStateManager { throw 'unreachable'; } if (!columnState.sort && !columnState.filter) { - delete this._state[columnIndex]; + delete this._state[column]; } this._changed.emit({ type: 'transforms-updated', @@ -174,13 +189,13 @@ export class TransformStateManager { /** * Returns the transform metadata for the provided column. * - * @param columnIndex - The column index of the metadata to be retrieved. + * @param column - The column index of the metadata to be retrieved. */ - metadata(columnIndex: number): TransformStateManager.IColumn | undefined { - if (!this._state.hasOwnProperty(columnIndex)) { + metadata(column: string): TransformStateManager.IColumn | undefined { + if (!this._state.hasOwnProperty(column)) { return undefined; } else { - return this._state[columnIndex]; + return this._state[column]; } } @@ -218,12 +233,12 @@ export class TransformStateManager { return transforms; } - getFilterTransform(columnIndex: number): Transform.TransformSpec | undefined { - if (!this._state.hasOwnProperty(columnIndex)) { + getFilterTransform(column: string): Transform.TransformSpec | undefined { + if (!this._state.hasOwnProperty(column)) { return undefined; } - return this._state[columnIndex].filter; + return this._state[column].filter; } private _state: TransformStateManager.IState = {}; diff --git a/js/core/valueRenderer.ts b/js/core/valueRenderer.ts index 2936a3b3..8f28b442 100644 --- a/js/core/valueRenderer.ts +++ b/js/core/valueRenderer.ts @@ -140,7 +140,7 @@ export class FilterValueRenderer extends TextRenderer { return ( this._stateManager.has( this._dialog.region, - this._dialog.columnIndex, + this._dialog.column, config.value, ) || (!this._dialog.hasFilter && !this._dialog.userInteractedWithDialog) diff --git a/js/core/view.ts b/js/core/view.ts index 45ec98d0..05b3daa3 100644 --- a/js/core/view.ts +++ b/js/core/view.ts @@ -158,11 +158,10 @@ export class View { * Returns a Promise that resolves to an array of unique values contained in * the provided column index. * - * @param columnIndex - The index to retrieve unique values for. + * @param column - The column to retrieve unique values for. */ - uniqueValues(region: DataModel.CellRegion, columnIndex: number): any[] { - const columnName = this.metadata(region, 0, columnIndex)['name']; - return Array.from(new Set(this.dataset.data[columnName])); + uniqueValues(region: DataModel.CellRegion, column: string): any[] { + return Array.from(new Set(this.dataset.data[column])); } private readonly _data: DataSource | Readonly; diff --git a/js/core/viewbasedjsonmodel.ts b/js/core/viewbasedjsonmodel.ts index b68cda1b..37802479 100644 --- a/js/core/viewbasedjsonmodel.ts +++ b/js/core/viewbasedjsonmodel.ts @@ -377,14 +377,14 @@ export class ViewBasedJSONModel extends MutableDataModel { /** * Get the current View for the model. */ - protected get currentView(): View { + get currentView(): View { return this._currentView; } /** * Sets the provided View as the current View, then emits a changed signal. */ - protected set currentView(view: View) { + set currentView(view: View) { this._currentView = view; this.emitChanged({ type: 'model-reset' }); @@ -422,12 +422,12 @@ export class ViewBasedJSONModel extends MutableDataModel { /** * Removes the provided transformation from the active state. * - * @param columnIndex - The index of the column state to be removed. + * @param column - The column state to be removed. * * @param transformType - The type of the transform to be removed from state. */ - removeTransform(columnIndex: number, transformType: string): void { - this._transformState.remove(columnIndex, transformType); + removeTransform(column: string, transformType: string): void { + this._transformState.remove(column, transformType); } /** @@ -450,12 +450,10 @@ export class ViewBasedJSONModel extends MutableDataModel { /** * Returns the transform metadata for the provided column. * - * @param columnIndex - The column index of the metadata to be retrieved. + * @param column - The column of the metadata to be retrieved. */ - transformMetadata( - columnIndex: number, - ): TransformStateManager.IColumn | undefined { - return this._transformState.metadata(columnIndex); + transformMetadata(column: string): TransformStateManager.IColumn | undefined { + return this._transformState.metadata(column); } /** @@ -463,11 +461,10 @@ export class ViewBasedJSONModel extends MutableDataModel { * the provided column index. * * @param region - The CellRegion to retrieve unique values for. - * @param columnIndex - The index to retrieve unique values for. + * @param column - The column to retrieve unique values for. */ - uniqueValues(region: DataModel.CellRegion, columnIndex: number): any[] { - const columnName = this.metadata(region, 0, columnIndex)['name']; - return Array.from(new Set(this.dataset.data[columnName])); + uniqueValues(region: DataModel.CellRegion, column: string): any[] { + return Array.from(new Set(this.dataset.data[column])); } /** @@ -475,13 +472,13 @@ export class ViewBasedJSONModel extends MutableDataModel { * the provided column index after all transforms have been applied. * * @param region - The CellRegion to retrieve unique values for. - * @param columnIndex - The index to retrieve unique values for. + * @param column - The column to retrieve unique values for. */ async uniqueValuesVisible( region: DataModel.CellRegion, - columnIndex: number, + column: string, ): Promise { - return this._currentView.uniqueValues(region, columnIndex); + return this._currentView.uniqueValues(region, column); } get transformStateChanged(): ISignal { @@ -495,8 +492,8 @@ export class ViewBasedJSONModel extends MutableDataModel { return this._transformState.activeTransforms; } - getFilterTransform(columnIndex: number): Transform.TransformSpec | undefined { - return this._transformState.getFilterTransform(columnIndex); + getFilterTransform(column: string): Transform.TransformSpec | undefined { + return this._transformState.getFilterTransform(column); } /** diff --git a/js/datasource.ts b/js/datasource.ts index dfbb1684..29baf46c 100644 --- a/js/datasource.ts +++ b/js/datasource.ts @@ -32,6 +32,10 @@ export class DataSource { return this._schema; } + get columns(): string[] { + return this._columns; + } + get length(): number { if (this._columns.length == 0) { return 0; diff --git a/js/feathergrid.ts b/js/feathergrid.ts index 71a2b494..8222d68c 100644 --- a/js/feathergrid.ts +++ b/js/feathergrid.ts @@ -879,14 +879,19 @@ export class FeatherGrid extends Widget { iconClass: 'ipydatagrid-filterMenuIcon ipydatagrid-filterMenuIcon-sortAsc', execute: (args): void => { - const cellClick: FeatherGridContextMenu.CommandArgs = + const command: FeatherGridContextMenu.CommandArgs = args as FeatherGridContextMenu.CommandArgs; + const colIndex = this._dataModel.getSchemaIndex( - cellClick.region, - cellClick.columnIndex, + command.region, + command.columnIndex, ); + + const column = this.dataModel.currentView.dataset.columns[colIndex]; + this._dataModel.addTransform({ type: 'sort', + column, columnIndex: colIndex, desc: false, }); @@ -898,14 +903,19 @@ export class FeatherGrid extends Widget { iconClass: 'ipydatagrid-filterMenuIcon ipydatagrid-filterMenuIcon-sortDesc', execute: (args) => { - const cellClick: FeatherGridContextMenu.CommandArgs = + const command: FeatherGridContextMenu.CommandArgs = args as FeatherGridContextMenu.CommandArgs; + const colIndex = this._dataModel.getSchemaIndex( - cellClick.region, - cellClick.columnIndex, + command.region, + command.columnIndex, ); + + const column = this.dataModel.currentView.dataset.columns[colIndex]; + this._dataModel.addTransform({ type: 'sort', + column, columnIndex: colIndex, desc: true, }); @@ -915,12 +925,16 @@ export class FeatherGrid extends Widget { label: 'Clear This Filter', mnemonic: -1, execute: (args) => { - const commandArgs = args; - const schemaIndex: number = this._dataModel.getSchemaIndex( - commandArgs.region, - commandArgs.columnIndex, + const command = args; + + const colIndex = this._dataModel.getSchemaIndex( + command.region, + command.columnIndex, ); - this._dataModel.removeTransform(schemaIndex, 'filter'); + + const column = this.dataModel.currentView.dataset.columns[colIndex]; + + this._dataModel.removeTransform(column, 'filter'); }, }); commands.addCommand( @@ -947,10 +961,19 @@ export class FeatherGrid extends Widget { 'ipydatagrid-filterMenuIcon ipydatagrid-filterMenuIcon-filter', execute: (args) => { const commandArgs = args; + + const colIndex = this._dataModel.getSchemaIndex( + commandArgs.region, + commandArgs.columnIndex, + ); + + const column = this.dataModel.currentView.dataset.columns[colIndex]; + this._filterDialog.open({ x: commandArgs.clientX, y: commandArgs.clientY, region: commandArgs.region, + column, columnIndex: commandArgs.columnIndex, forceX: false, forceY: false, @@ -968,10 +991,19 @@ export class FeatherGrid extends Widget { 'ipydatagrid-filterMenuIcon ipydatagrid-filterMenuIcon-filter', execute: (args) => { const commandArgs = args; + + const colIndex = this._dataModel.getSchemaIndex( + commandArgs.region, + commandArgs.columnIndex, + ); + + const column = this.dataModel.currentView.dataset.columns[colIndex]; + this._filterDialog.open({ x: commandArgs.clientX, y: commandArgs.clientY, region: commandArgs.region, + column, columnIndex: commandArgs.columnIndex, forceX: false, forceY: false, @@ -1020,11 +1052,15 @@ export class FeatherGrid extends Widget { mnemonic: 1, execute: (args) => { const commandArgs = args; - const schemaIndex: number = this._dataModel.getSchemaIndex( + + const colIndex = this._dataModel.getSchemaIndex( commandArgs.region, commandArgs.columnIndex, ); - this._dataModel.removeTransform(schemaIndex, 'sort'); + + const column = this.dataModel.currentView.dataset.columns[colIndex]; + + this._dataModel.removeTransform(column, 'sort'); }, }); commands.addCommand(FeatherGridContextMenu.CommandID.ClearSelection, { diff --git a/js/utils.ts b/js/utils.ts index 2dfea27d..095baf69 100644 --- a/js/utils.ts +++ b/js/utils.ts @@ -105,15 +105,16 @@ export namespace ArrayUtils { const curVal = data[primaryKey[i]][j]; // if (curMergedRange.length == 0 || prevVal == curVal) { const [parentGroupStart, parentGroupEnd] = getParentGroupPosition( - retArr, - j, - i, - ); + retArr, + j, + i, + ); if ( - curMergedRange.length == 0 || (prevVal == curVal) && + curMergedRange.length == 0 || + (prevVal == curVal && curMergedRange[0][0] >= parentGroupStart && - j <= parentGroupEnd - ) { + j <= parentGroupEnd) + ) { curMergedRange.push([j, i]); } else { curCol.push(curMergedRange); @@ -142,10 +143,12 @@ export namespace ArrayUtils { rowNum: number, colNum: number, ): number[] { - if (colNum === 0) {return [0, rowNum]}; - for (let i = 0; i < retArr[colNum-1].length; i++) { + if (colNum === 0) { + return [0, rowNum]; + } + for (let i = 0; i < retArr[colNum - 1].length; i++) { // iterate mergegroups of previous row - const curMergedGroup = retArr[colNum-1][i]; + const curMergedGroup = retArr[colNum - 1][i]; const curMergedGroupLen = curMergedGroup.length; const firstRow = curMergedGroup[0][0]; const lastRow = curMergedGroup[curMergedGroupLen - 1][0]; diff --git a/tests/js/datagrid.test.ts b/tests/js/datagrid.test.ts index 40675050..5ec2521c 100644 --- a/tests/js/datagrid.test.ts +++ b/tests/js/datagrid.test.ts @@ -138,7 +138,7 @@ describe('Test trait: data', () => { const testData = Private.createBasicTestData(); const transform: Transform.TransformSpec = { type: 'sort', - columnIndex: 0, + column: 'index', desc: true, }; const grid = await Private.createGridWidget({ @@ -149,11 +149,11 @@ describe('Test trait: data', () => { }, }); const oldTransforms = grid.model.data_model.transformMetadata( - transform.columnIndex, + transform.column, ); grid.model.set('_data', testData.set2.data); expect( - grid.model.data_model.transformMetadata(transform.columnIndex), + grid.model.data_model.transformMetadata(transform.column), ).toEqual(oldTransforms); }); diff --git a/tests/js/filterMenu.test.ts b/tests/js/filterMenu.test.ts index 9b98c692..796d98a7 100644 --- a/tests/js/filterMenu.test.ts +++ b/tests/js/filterMenu.test.ts @@ -31,18 +31,17 @@ describe('Test .hasValidFilterValue()', () => { describe('Test .applyFilter()', () => { test('.addTransform() is called', () => { const dialog = Private.createSimpleDialog(); - const colIndex = 0; const transform: Transform.TransformSpec = { type: 'filter', - columnIndex: colIndex + 1, + column: 'test', operator: '=', value: 6, }; Private.setDialogState({ dialog: dialog, - columnIndex: colIndex, + column: transform.column, mode: 'condition', operator: transform.operator, region: 'body', @@ -55,18 +54,17 @@ describe('Test .applyFilter()', () => { }); test('condition transform is added', () => { const dialog = Private.createSimpleDialog(); - const colIndex = 0; const transform: Transform.TransformSpec = { type: 'filter', - columnIndex: colIndex + 1, + column: 'test', operator: '=', value: 6, }; Private.setDialogState({ dialog: dialog, - columnIndex: colIndex, + column: transform.column, mode: 'condition', operator: transform.operator, region: 'body', @@ -74,25 +72,24 @@ describe('Test .applyFilter()', () => { }); dialog.applyFilter(); - const addedTransform = dialog.model.transformMetadata(colIndex + 1)![ + const addedTransform = dialog.model.transformMetadata(transform.column)![ 'filter' ]; expect(addedTransform).toEqual(transform); }); test('value transform is added', () => { const dialog = Private.createSimpleDialog(); - const colIndex = 0; const transform: Transform.TransformSpec = { type: 'filter', - columnIndex: colIndex + 1, + column: 'test', operator: 'in', value: [], }; Private.setDialogState({ dialog: dialog, - columnIndex: colIndex, + column: transform.column, mode: 'value', operator: transform.operator, region: 'body', @@ -100,7 +97,7 @@ describe('Test .applyFilter()', () => { }); dialog.userInteractedWithDialog = true; dialog.applyFilter(); - const addedTransform = dialog.model.transformMetadata(colIndex + 1)![ + const addedTransform = dialog.model.transformMetadata(transform.column)![ 'filter' ]; expect(addedTransform).toEqual(transform); @@ -115,7 +112,7 @@ describe('Test .updateDialog()', () => { dialog: dialog, region: 'body', value: 6, - columnIndex: 0, + column: 'test', mode: 'value', operator: '<', }); @@ -133,12 +130,13 @@ describe('Test .updateDialog()', () => { const dialog = Private.createSimpleDialog(); const transform: Transform.TransformSpec = { type: 'filter', - columnIndex: 1, + column: 'test', operator: '>=', value: 6, }; dialog.model.addTransform(transform); dialog.open({ + column: 'test', columnIndex: 0, region: 'body', mode: 'condition', @@ -156,7 +154,8 @@ describe('.open()', () => { test('open event updates state', () => { const dialog = Private.createSimpleDialog(); const openOptions: InteractiveFilterDialog.IOpenOptions = { - columnIndex: 0, + column: 'test', + columnIndex: 1, forceX: false, forceY: false, mode: 'condition', @@ -165,12 +164,8 @@ describe('.open()', () => { y: 0, }; dialog.open(openOptions); - expect(dialog.columnIndex).toBe(openOptions.columnIndex); - expect(dialog.columnDType).toBe( - dialog.model.metadata(openOptions.region, 0, openOptions.columnIndex)[ - 'type' - ], - ); + expect(dialog.column).toBe(openOptions.column); + expect(dialog.columnDType).toBe('number'); }); }); @@ -214,7 +209,7 @@ namespace Private { // @ts-ignore dialog._filterValue = options.value; // @ts-ignore - dialog._columnIndex = options.columnIndex; + dialog._column = options.column; // @ts-ignore dialog._mode = options.mode; // @ts-ignore @@ -276,8 +271,8 @@ namespace Private { mode: InteractiveFilterDialog.FilterMode; /** - * The active column index to set. + * The active column to set. */ - columnIndex: number; + column: string; } } diff --git a/tests/js/transformStateManager.test.ts b/tests/js/transformStateManager.test.ts index 2b9cdcc9..b2a5b244 100644 --- a/tests/js/transformStateManager.test.ts +++ b/tests/js/transformStateManager.test.ts @@ -5,8 +5,8 @@ import { View } from '../../js/core/view'; describe('Test .add()', () => { const testCases: Transform.TransformSpec[] = [ - { type: 'sort', columnIndex: 3, desc: true }, - { type: 'filter', columnIndex: 2, operator: '<', value: 5 }, + { type: 'sort', column: 'test', desc: true }, + { type: 'filter', column: 'test', operator: '<', value: 5 }, ]; testCases.forEach((testCase) => { test(`State is update: ${testCase.type}-${testCase.columnIndex}`, () => { @@ -20,13 +20,13 @@ describe('Test .add()', () => { const transform1 = Private.simpleSort(); const transform2: Transform.Sort = { type: 'sort', - columnIndex: transform1.columnIndex + 1, + column: 'test2', desc: true, }; tsm.add(transform1); tsm.add(transform2); // @ts-ignore - expect(tsm._state[transform1.columnIndex][transform1.type]).toBeUndefined(); + expect(tsm._state[transform1.column][transform1.type]).toBeUndefined(); }); test('Clear state on error', () => { const tsm = new TransformStateManager(); @@ -69,7 +69,7 @@ describe('Test .replace()', () => { const state = Private.simpleState(); const transform3: Transform.TransformSpec = { type: 'filter', - columnIndex: 2, + column: 'test', operator: '=', value: 765, }; @@ -131,36 +131,36 @@ describe('Test .remove()', () => { const state = Private.simpleState(); const tsm = new TransformStateManager(); tsm.replace([state.sort!, state.filter!]); - tsm.remove(state.filter!.columnIndex, state.filter!.type); + tsm.remove(state.filter!.column, state.filter!.type); expect(tsm.activeTransforms).toEqual([state.sort]); }); test('Remove filter transform', () => { const state = Private.simpleState(); const tsm = new TransformStateManager(); tsm.replace([state.sort!, state.filter!]); - tsm.remove(state.filter!.columnIndex, state.filter!.type); + tsm.remove(state.filter!.column, state.filter!.type); expect(tsm.activeTransforms).toEqual([state.sort]); }); test('State cleared after error', () => { const state = Private.simpleState(); const tsm = new TransformStateManager(); tsm.replace([state.sort!, state.filter!]); - tsm.remove(state.filter!.columnIndex, 'transformThatDoesntExist'); + tsm.remove(state.filter!.column, 'transformThatDoesntExist'); expect(tsm.activeTransforms).toEqual([]); }); test('State entry cleared if no sort or filter', () => { const transform1 = Private.simpleSort(); const tsm = new TransformStateManager(); tsm.add(transform1); - tsm.remove(transform1.columnIndex, transform1.type); + tsm.remove(transform1.column, transform1.type); // @ts-ignore - expect(tsm._state[transform1.columnIndex]).toBeUndefined(); + expect(tsm._state[transform1.column]).toBeUndefined(); }); test('No state change on invalid column', () => { const state = Private.simpleState(); const tsm = new TransformStateManager(); tsm.replace([state.sort!, state.filter!]); - tsm.remove(state.filter!.columnIndex + 1, state.filter!.type); + tsm.remove(state.filter!.column + 1, state.filter!.type); expect(tsm.activeTransforms).toEqual([state.sort!, state.filter!]); }); }); @@ -171,16 +171,16 @@ describe('Test .metadata()', () => { const tsm = new TransformStateManager(); tsm.add(transform1); const test = { filter: undefined, sort: transform1 }; - expect(tsm.metadata(transform1.columnIndex)).toEqual(test); - expect(tsm.metadata(transform1.columnIndex + 1)).toBeUndefined(); + expect(tsm.metadata(transform1.column)).toEqual(test); + expect(tsm.metadata(transform1.column + 1)).toBeUndefined(); }); test('Get metadata for column - filter', () => { const transform1 = Private.simpleFilter(); const tsm = new TransformStateManager(); tsm.add(transform1); const test = { filter: transform1, sort: undefined }; - expect(tsm.metadata(transform1.columnIndex)).toEqual(test); - expect(tsm.metadata(transform1.columnIndex + 1)).toBeUndefined(); + expect(tsm.metadata(transform1.column)).toEqual(test); + expect(tsm.metadata(transform1.column + 1)).toBeUndefined(); }); }); @@ -219,14 +219,14 @@ namespace Private { * Returns a simple sort transform. */ export function simpleSort(): Transform.Sort { - return { type: 'sort', columnIndex: 1, desc: true }; + return { type: 'sort', column: 'test', desc: true }; } /** * Returns a simple filter transform. */ export function simpleFilter(): Transform.Filter { - return { type: 'filter', columnIndex: 1, operator: '<', value: 0 }; + return { type: 'filter', column: 'test', operator: '<', value: 0 }; } /** diff --git a/tests/js/viewbasedjsonmodel.test.ts b/tests/js/viewbasedjsonmodel.test.ts index 86a4aeac..a496181c 100644 --- a/tests/js/viewbasedjsonmodel.test.ts +++ b/tests/js/viewbasedjsonmodel.test.ts @@ -44,7 +44,7 @@ describe('Test interactions with TransformStateManager', () => { let mock = jest.spyOn(TransformStateManager.prototype, 'add'); const transform: Transform.TransformSpec = { type: 'sort', - columnIndex: 0, + column: 'index', desc: true, }; model.addTransform(transform); @@ -53,8 +53,8 @@ describe('Test interactions with TransformStateManager', () => { test('.removeTransform()', () => { const model = Private.createSimpleModel(); let mock = jest.spyOn(TransformStateManager.prototype, 'remove'); - model.removeTransform(0, 'sort'); - expect(mock).toBeCalledWith(0, 'sort'); + model.removeTransform('index', 'sort'); + expect(mock).toBeCalledWith('index', 'sort'); }); test('.replaceTransform()', () => { const model = Private.createSimpleModel(); @@ -62,7 +62,7 @@ describe('Test interactions with TransformStateManager', () => { const transforms: Transform.TransformSpec[] = [ { type: 'sort', - columnIndex: 0, + column: 'index', desc: true, }, ]; @@ -78,8 +78,8 @@ describe('Test interactions with TransformStateManager', () => { test('.transformMetadata()', () => { const model = Private.createSimpleModel(); let mock = jest.spyOn(TransformStateManager.prototype, 'metadata'); - model.transformMetadata(0); - expect(mock).toBeCalledWith(0); + model.transformMetadata('index'); + expect(mock).toBeCalledWith('index'); }); }); @@ -135,7 +135,7 @@ describe('Test .uniqueValues()', () => { }); const testModel = new ViewBasedJSONModel(testData.data); test('cellregion-column-header-0', () => { - expect(testModel.uniqueValues('column-header', 0)).toEqual([ + expect(testModel.uniqueValues('column-header', 'col1')).toEqual([ 10, 20, 30, @@ -144,20 +144,20 @@ describe('Test .uniqueValues()', () => { ]); }); test('cellregion-column-header-1', () => { - expect(testModel.uniqueValues('column-header', 1)).toEqual([ + expect(testModel.uniqueValues('column-header', 'col2')).toEqual([ true, false, ]); }); test('cellregion-column-header-2', () => { - expect(testModel.uniqueValues('column-header', 2)).toEqual([ + expect(testModel.uniqueValues('column-header', 'col3')).toEqual([ 100, 200, 300, ]); }); test('cellregion-corner-header-0', () => { - expect(testModel.uniqueValues('corner-header', 0)).toEqual([ + expect(testModel.uniqueValues('corner-header', 'index')).toEqual([ 'A', 'C', 'B', diff --git a/ui-tests-ipw7/tests/notebooks/datagrid.ipynb b/ui-tests-ipw7/tests/notebooks/datagrid.ipynb index 0223a1d2..52d7ddd5 100644 --- a/ui-tests-ipw7/tests/notebooks/datagrid.ipynb +++ b/ui-tests-ipw7/tests/notebooks/datagrid.ipynb @@ -50,7 +50,7 @@ "}\n", "\n", "grid = DataGrid(df, base_row_size=30, base_column_size=300, renderers=renderers)\n", - "grid.transform([{\"type\": \"sort\", \"columnIndex\": 2, \"desc\": True}])\n", + "grid.transform([{\"type\": \"sort\", \"column\": \"Value 2\", \"desc\": True}])\n", "grid" ] }, @@ -81,7 +81,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.9" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/dark-datagrid-ipynb-cell-0-linux.png b/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/dark-datagrid-ipynb-cell-0-linux.png index c48557cb..b4c13914 100644 Binary files a/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/dark-datagrid-ipynb-cell-0-linux.png and b/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/dark-datagrid-ipynb-cell-0-linux.png differ diff --git a/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/dark-datagrid-ipynb-cell-1-linux.png b/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/dark-datagrid-ipynb-cell-1-linux.png index b50923a4..48d4a6c5 100644 Binary files a/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/dark-datagrid-ipynb-cell-1-linux.png and b/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/dark-datagrid-ipynb-cell-1-linux.png differ diff --git a/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/light-datagrid-ipynb-cell-0-linux.png b/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/light-datagrid-ipynb-cell-0-linux.png index 38ffdb63..4b6d624b 100644 Binary files a/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/light-datagrid-ipynb-cell-0-linux.png and b/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/light-datagrid-ipynb-cell-0-linux.png differ diff --git a/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/light-datagrid-ipynb-cell-1-linux.png b/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/light-datagrid-ipynb-cell-1-linux.png index a0a1c429..038735fe 100644 Binary files a/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/light-datagrid-ipynb-cell-1-linux.png and b/ui-tests-ipw8/tests/ipydatagrid.test.ts-snapshots/light-datagrid-ipynb-cell-1-linux.png differ diff --git a/ui-tests-ipw8/tests/notebooks/datagrid.ipynb b/ui-tests-ipw8/tests/notebooks/datagrid.ipynb index e544ca8b..bf1e710a 100644 --- a/ui-tests-ipw8/tests/notebooks/datagrid.ipynb +++ b/ui-tests-ipw8/tests/notebooks/datagrid.ipynb @@ -50,6 +50,7 @@ "}\n", "\n", "grid = DataGrid(df, base_row_size=30, base_column_size=300, renderers=renderers)\n", + "# Setting \"columnIndex\" is deprecated but it should still work\n", "grid.transform([{\"type\": \"sort\", \"columnIndex\": 2, \"desc\": True}])\n", "grid" ] @@ -63,16 +64,6 @@ "source": [ "grid" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5d083622-1461-4da1-aba9-2e5e8540e048", - "metadata": {}, - "outputs": [], - "source": [ - "grid.data.columns" - ] } ], "metadata": { diff --git a/ui-tests-ipw8/tests/notebooks/datagrid_nested_hierarchies.ipynb b/ui-tests-ipw8/tests/notebooks/datagrid_nested_hierarchies.ipynb index 43b5d5c6..6e4a324d 100644 --- a/ui-tests-ipw8/tests/notebooks/datagrid_nested_hierarchies.ipynb +++ b/ui-tests-ipw8/tests/notebooks/datagrid_nested_hierarchies.ipynb @@ -61,7 +61,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.12.3" } }, "nbformat": 4,