Skip to content

Commit

Permalink
feat: "Group" column for rollup/tree tables (#1636)
Browse files Browse the repository at this point in the history
- Have a virtual column that just shows the rolled up value
- Displays the value from the "source" cell (ie. the value from the
grouping column at that depth)
- The context menu shows the filter options for the source cell, so you
can filter by options
- Virtual column you cannot use quick filters or sort on
- Closes #1555
- Tested with rollup from docs:
```py
from deephaven import read_csv, agg

insurance = read_csv("https://media.githubusercontent.com/media/deephaven/examples/main/Insurance/csv/insurance.csv")

agg_list = [agg.avg(cols=["bmi", "expenses"])]
by_list = ["region", "age"]

test_rollup = insurance.rollup(aggs=[], by=by_list, include_constituents=True)
insurance_rollup = insurance.rollup(aggs=agg_list, by=by_list, include_constituents=True)
```
  • Loading branch information
mofojed authored Dec 15, 2023
1 parent 9d519e1 commit ba1d51b
Show file tree
Hide file tree
Showing 34 changed files with 403 additions and 178 deletions.
14 changes: 9 additions & 5 deletions packages/grid/src/GridRange.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
export type GridRangeIndex = number | null;
export type RequiredGridRangeIndex = number;
type LeftIndex = GridRangeIndex;
type RightIndex = GridRangeIndex;
type TopIndex = GridRangeIndex;
type BottomIndex = GridRangeIndex;

export type GridCell = { column: number; row: number };
export type GridCell = {
column: RequiredGridRangeIndex;
row: RequiredGridRangeIndex;
};

export interface BoundedGridRange extends GridRange {
startColumn: number;
startRow: number;
endColumn: number;
endRow: number;
startColumn: RequiredGridRangeIndex;
startRow: RequiredGridRangeIndex;
endColumn: RequiredGridRangeIndex;
endRow: RequiredGridRangeIndex;
}

// Also exported via GridRange.SELECTION_DIRECTION
Expand Down
17 changes: 7 additions & 10 deletions packages/iris-grid/src/ColumnStatistics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@ import classNames from 'classnames';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, CopyButton, LoadingSpinner } from '@deephaven/components';
import { dhFreeze, dhRefresh, dhSortSlash, vsLock } from '@deephaven/icons';
import type {
Column,
ColumnStatistics as APIColumnStatistics,
} from '@deephaven/jsapi-types';
import type { ColumnStatistics as APIColumnStatistics } from '@deephaven/jsapi-types';
import Log from '@deephaven/log';
import { CancelablePromise, PromiseUtils } from '@deephaven/utils';
import { isExpandableGridModel } from '@deephaven/grid';
import './ColumnStatistics.scss';
import IrisGridModel from './IrisGridModel';
import IrisGridModel, { DisplayColumn } from './IrisGridModel';

const log = Log.module('ColumnStatistics');
const STATS_LABEL_OVERRIDES: Record<string, string> = {
Expand All @@ -26,14 +23,14 @@ interface Statistic {
}

interface ColumnStatisticsProps {
column: Column;
column: DisplayColumn;
model: IrisGridModel;
onStatistics: () => void;
}
interface ColumnStatisticsState {
error: unknown;
loading: boolean;
statistics: Statistic[] | null;
statistics: readonly Statistic[] | null;
numRows: number;
}

Expand Down Expand Up @@ -84,15 +81,15 @@ class ColumnStatistics extends Component<
cancelablePromise: CancelablePromise<APIColumnStatistics> | null;

maybeGenerateStatistics(): void {
const { model } = this.props;
const { column, model } = this.props;

const numRows =
model.rowCount -
model.pendingRowCount -
model.floatingBottomRowCount -
model.floatingTopRowCount;
this.setState({ numRows });
if (!model.isColumnStatisticsAvailable) {
if (!model.isColumnStatisticsAvailable || column.isProxy === true) {
this.setState({ loading: false });
} else if (numRows < ColumnStatistics.AUTO_GENERATE_LIMIT) {
this.handleGenerateStatistics();
Expand Down Expand Up @@ -200,7 +197,7 @@ class ColumnStatistics extends Component<
return (
<div className="column-statistics">
<div className="column-statistics-title">
{column.name}
{column.displayName ?? column.name}
<span className="column-statistics-type">&nbsp;({columnType})</span>
<CopyButton
className="column-statistics-copy"
Expand Down
39 changes: 22 additions & 17 deletions packages/iris-grid/src/CrossColumnSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
import { TableUtils } from '@deephaven/jsapi-utils';
import './CrossColumnSearch.scss';
import { ColumnName } from './CommonTypes';
import { DisplayColumn } from './IrisGridModel';

interface CrossColumnSearchProps {
value: string;
Expand All @@ -27,7 +28,7 @@ interface CrossColumnSearchProps {
selectedColumns: readonly ColumnName[],
invertSelection: boolean
) => void;
columns: readonly Column[];
columns: readonly DisplayColumn[];
}

interface CrossColumnSearchState {
Expand Down Expand Up @@ -233,23 +234,27 @@ class CrossColumnSearch extends PureComponent<
<div className="cross-column-popup">
Searched Columns
<div className="cross-column-scroll">
{columns.map(column => (
<React.Fragment key={column.name}>
<Checkbox
className="cross-column-checkbox"
checked={
invertSelection
? !selectedColumns.includes(column.name)
: selectedColumns.includes(column.name)
}
onChange={() => this.toggleColumn(column.name)}
>
{column.name}
</Checkbox>
{columns.map(column => {
if (column.isProxy === true) return null;

{column.type.substring(column.type.lastIndexOf('.') + 1)}
</React.Fragment>
))}
return (
<React.Fragment key={column.name}>
<Checkbox
className="cross-column-checkbox"
checked={
invertSelection
? !selectedColumns.includes(column.name)
: selectedColumns.includes(column.name)
}
onChange={() => this.toggleColumn(column.name)}
>
{column.name}
</Checkbox>

{column.type.substring(column.type.lastIndexOf('.') + 1)}
</React.Fragment>
);
})}
</div>
<div className="cross-column-button-bar">
<button
Expand Down
43 changes: 28 additions & 15 deletions packages/iris-grid/src/IrisGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,20 +171,21 @@ import { ChartBuilderSettings } from './sidebar/ChartBuilder';
import AggregationOperation from './sidebar/aggregations/AggregationOperation';
import { UIRollupConfig } from './sidebar/RollupRows';
import {
ReadonlyAdvancedFilterMap,
ColumnName,
ReadonlyQuickFilterMap,
ReadonlyAggregationMap,
ReadonlyOperationMap,
Action,
OptionItem,
UITotalsTableConfig,
InputFilter,
PendingDataMap,
AdvancedFilterMap,
AdvancedFilterOptions,
ColumnName,
InputFilter,
OperationMap,
OptionItem,
PendingDataErrorMap,
PendingDataMap,
QuickFilterMap,
OperationMap,
ReadonlyAdvancedFilterMap,
ReadonlyAggregationMap,
ReadonlyOperationMap,
ReadonlyQuickFilterMap,
UITotalsTableConfig,
} from './CommonTypes';
import ColumnHeaderGroup from './ColumnHeaderGroup';
import { IrisGridThemeContext } from './IrisGridThemeProvider';
Expand Down Expand Up @@ -1636,19 +1637,31 @@ export class IrisGrid extends Component<IrisGridProps, IrisGridState> {
});
}

removeColumnFilter(modelColumn: ModelIndex): void {
removeColumnFilter(modelRange: ModelIndex | BoundedAxisRange): void {
this.startLoading('Filtering...', true);

const clearRange: BoundedAxisRange = Array.isArray(modelRange)
? modelRange
: [modelRange, modelRange];

this.setState(
({ advancedFilters, quickFilters }: Partial<IrisGridState>) => {
const newAdvancedFilters = advancedFilters
const newAdvancedFilters: AdvancedFilterMap = advancedFilters
? new Map(advancedFilters)
: new Map();
const newQuickFilters = quickFilters
const newQuickFilters: QuickFilterMap = quickFilters
? new Map(quickFilters)
: new Map();
newQuickFilters.delete(modelColumn);
newAdvancedFilters.delete(modelColumn);
newAdvancedFilters.forEach((_, column) => {
if (column >= clearRange[0] && column <= clearRange[1]) {
newAdvancedFilters.delete(column);
}
});
newQuickFilters.forEach((_, column) => {
if (column >= clearRange[0] && column <= clearRange[1]) {
newQuickFilters.delete(column);
}
});

return {
quickFilters: newQuickFilters,
Expand Down
77 changes: 63 additions & 14 deletions packages/iris-grid/src/IrisGridModel.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* eslint-disable class-methods-use-this */
import type { Event, EventTarget } from 'event-target-shim';
import {
BoundedAxisRange,
DataBarGridModel,
DataBarOptions,
GridCell,
GridModel,
GridRange,
GridThemeType,
Expand Down Expand Up @@ -33,6 +35,21 @@ import {
} from './CommonTypes';
import ColumnHeaderGroup from './ColumnHeaderGroup';

export type DisplayColumn = Column & {
/**
* Name to display with the column.
* The `name` property on `Column` is a unique identifier and must be a valid Java identifier,
* whereas `displayName` can be any string and does not need to be unique.
*/
displayName?: string;

/**
* Whether this column is a proxy column for other columns or not.
* If it's a proxy column, it may not appear in some lists.
*/
isProxy?: boolean;
};

type IrisGridModelEventNames =
(typeof IrisGridModel.EVENT)[keyof typeof IrisGridModel.EVENT];

Expand Down Expand Up @@ -133,7 +150,23 @@ abstract class IrisGridModel<
* Gets the columns for this model
* @returns All columns in the model
*/
abstract get columns(): readonly Column[];
abstract get columns(): readonly DisplayColumn[];

/**
* Retrieve the grouped columns for this model
* @returns The columns that are grouped
*/
get groupedColumns(): readonly DisplayColumn[] {
return EMPTY_ARRAY;
}

/**
* Gets the columns for the model before any transformations (such as rollups) are applied.
* @returns All original columns in the model.
*/
get originalColumns(): readonly DisplayColumn[] {
return this.columns;
}

/**
* Gets the column index for this model
Expand All @@ -143,26 +176,42 @@ abstract class IrisGridModel<
abstract getColumnIndexByName(name: string): ModelIndex | undefined;

/**
* Gets the columns for the model before any transformations (such as rollups) are applied.
* @returns All original columns in the model.
* Retrieve the source cell for a given cell. Returns something different if the cell is a proxied cell
* that retrieves data from another cell.
* @param column Column to get the source for
* @param row Row to get the source for
* @returns Source cell where the data is coming from
*/
get originalColumns(): readonly Column[] {
return this.columns;
sourceForCell(column: ModelIndex, row: ModelIndex): GridCell {
return { column, row };
}

abstract get initialMovedColumns(): readonly MoveOperation[];
/**
* Retrieve the range of columns to clear filters on for a given column.
* @param column Column to get the range of filters to clear.
* @returns Axis range of the column filters to clear, or `null` if this should not have a clear filter option.
*/
getClearFilterRange(column: ModelIndex): BoundedAxisRange | null {
if (this.isFilterable(column)) {
return [column, column];
}
return null;
}

/** List of column movements defined by the model. Used as initial movements for IrisGrid */
get initialMovedColumns(): readonly MoveOperation[] {
return EMPTY_ARRAY;
}

/** List of row movements defined by the model. Used as initial movements for IrisGrid */
abstract get initialMovedRows(): readonly MoveOperation[];
get initialMovedRows(): readonly MoveOperation[] {
return EMPTY_ARRAY;
}

/** List of column header groups defined by the model */
abstract get initialColumnHeaderGroups(): readonly ColumnHeaderGroup[];

/**
* Retrieve the grouped columns for this model
* @returns The columns that are groupe
*/
abstract get groupedColumns(): readonly Column[];
get initialColumnHeaderGroups(): readonly ColumnHeaderGroup[] {
return EMPTY_ARRAY;
}

/**
* @param column The model column index
Expand Down
6 changes: 6 additions & 0 deletions packages/iris-grid/src/IrisGridProxyModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,12 @@ class IrisGridProxyModel extends IrisGridModel {
return this.model.groupedColumns;
}

sourceForCell: IrisGridModel['sourceForCell'] = (...args) =>
this.model.sourceForCell(...args);

getClearFilterRange: IrisGridModel['getClearFilterRange'] = (...args) =>
this.model.getClearFilterRange(...args);

get description(): string {
return this.model.description;
}
Expand Down
Loading

0 comments on commit ba1d51b

Please sign in to comment.