Skip to content
This repository has been archived by the owner on Jun 4, 2024. It is now read-only.

Commit

Permalink
Issue 256, 257, 608 - Add conditional styling capabilities (#729)
Browse files Browse the repository at this point in the history
  • Loading branch information
Marc-Andre-Rivet authored Apr 21, 2020
1 parent 059103a commit 2ec0b43
Show file tree
Hide file tree
Showing 18 changed files with 471 additions and 111 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- [#729](https://github.com/plotly/dash-table/pull/729) Improve conditional styling
- `style_data_conditional`: Add support for `row_index` and `column_id` array of values
- `style_header_conditional`: Add support for `header_index` and `column_id` array of values
- `style_filter_conditional`: Add support for `column_id` array of values
- `style_cell_conditional`: Add support for `column_id` array of values
- `style_data_conditional`: Add new conditions `state: 'active'|'selected'` to customize selected and active cell styles

### Fixed
- [#722](https://github.com/plotly/dash-table/pull/722) Fix a bug where row height is misaligned when using fixed_columns and/or fixed_rows
- [#728](https://github.com/plotly/dash-table/pull/728) Fix copy/paste on readonly cells
Expand Down
14 changes: 9 additions & 5 deletions src/core/environment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import { Edge } from 'dash-table/derived/edges/type';
const DASH_DEBUG = 'dash_debug';
const DASH_LOG = 'dash_log';

const DEFAULT_EDGE: Edge = '1px solid #d3d3d3';
const ACTIVE_EDGE: Edge = '1px solid var(--accent)';

interface ISearchParams {
get: (key: string) => string | null;
}

export default class Environment {
private static readonly _supportsCssVariables = Boolean(window.CSS?.supports?.('.some-selector', 'var(--some-var)'));
private static readonly _activeEdge: Edge = Environment._supportsCssVariables ? '1px solid var(--accent)' : '1px solid hotpink';

public static get searchParams(): ISearchParams {
return (
typeof URL !== 'undefined' &&
Expand All @@ -40,10 +40,14 @@ export default class Environment {
}

public static get defaultEdge(): Edge {
return DEFAULT_EDGE;
return '1px solid #d3d3d3';
}

public static get activeEdge(): Edge {
return ACTIVE_EDGE;
return Environment._activeEdge;
}

public static get supportsCssVariables(): boolean {
return Environment._supportsCssVariables;
}
}
4 changes: 4 additions & 0 deletions src/dash-table/components/CellFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ export default class CellFactory {

const cellStyles = this.dataStyles(
partialCellStyles,
visibleColumns,
relevantStyles,
virtualized.data,
virtualized.offset,
active_cell,
selected_cells
);

Expand Down
25 changes: 19 additions & 6 deletions src/dash-table/components/EdgeFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as R from 'ramda';

import { memoizeOne } from 'core/memoizer';

import derivedDataEdges from 'dash-table/derived/edges/data';
import { derivedPartialDataEdges, derivedDataEdges } from 'dash-table/derived/edges/data';
import derivedDataOpEdges from 'dash-table/derived/edges/operationOfData';
import derivedFilterEdges from 'dash-table/derived/edges/filter';
import derivedFilterOpEdges from 'dash-table/derived/edges/operationOfFilters';
Expand All @@ -15,7 +15,7 @@ import getHeaderRows from 'dash-table/derived/header/headerRows';
import { derivedRelevantCellStyles, derivedRelevantFilterStyles, derivedRelevantHeaderStyles } from 'dash-table/derived/style';
import { Style, Cells, DataCells, BasicFilters, Headers } from 'dash-table/derived/style/props';

import { ControlledTableProps, Columns, IViewportOffset, Data, ICellCoordinates, TableAction } from './Table/props';
import { ControlledTableProps, Columns, IViewportOffset, Data, ICellCoordinates, TableAction, SelectedCells } from './Table/props';
import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree';

type EdgesMatricesOp = EdgesMatrices | undefined;
Expand All @@ -25,6 +25,7 @@ export default class EdgeFactory {
private readonly filterStyles = derivedRelevantFilterStyles();
private readonly headerStyles = derivedRelevantHeaderStyles();

private readonly getPartialDataEdges = derivedPartialDataEdges();
private readonly getDataEdges = derivedDataEdges();
private readonly getDataOpEdges = derivedDataOpEdges();
private readonly getFilterEdges = derivedFilterEdges();
Expand Down Expand Up @@ -146,6 +147,7 @@ export default class EdgeFactory {
fixed_rows,
row_deletable,
row_selectable,
selected_cells,
style_as_list_view,
style_cell,
style_cell_conditional,
Expand All @@ -168,6 +170,7 @@ export default class EdgeFactory {
workFilter.map,
fixed_columns,
fixed_rows,
selected_cells,
style_as_list_view,
style_cell,
style_cell_conditional,
Expand All @@ -191,6 +194,7 @@ export default class EdgeFactory {
filterMap: Map<string, SingleColumnSyntaxTree>,
fixed_columns: number,
fixed_rows: number,
selected_cells: SelectedCells,
style_as_list_view: boolean,
style_cell: Style,
style_cell_conditional: Cells,
Expand All @@ -200,7 +204,7 @@ export default class EdgeFactory {
style_filter_conditional: BasicFilters,
style_header: Style,
style_header_conditional: Headers,
data: Data,
virtualizedData: Data,
offset: IViewportOffset
) => {
const dataStyles = this.dataStyles(
Expand All @@ -226,19 +230,28 @@ export default class EdgeFactory {

const headerRows = getHeaderRows(columns);

const partialDataEdges = this.getPartialDataEdges(
visibleColumns,
dataStyles,
virtualizedData,
offset,
style_as_list_view
)

let dataEdges = this.getDataEdges(
partialDataEdges,
visibleColumns,
dataStyles,
data,
virtualizedData,
offset,
active_cell,
style_as_list_view
selected_cells
);

let dataOpEdges = this.getDataOpEdges(
operations,
dataStyles,
data,
virtualizedData,
offset,
style_as_list_view
);
Expand Down
9 changes: 5 additions & 4 deletions src/dash-table/components/Table/derivedPropsHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import QuerySyntaxTree from 'dash-table/syntax-tree/QuerySyntaxTree';

import {
ControlledTableProps,
IndexedData,
SanitizedAndDerivedProps,
SetProps,
TableAction
Expand Down Expand Up @@ -67,19 +68,19 @@ export default () => {
if (!virtualCached) {
newProps.derived_virtual_data = virtual.data;
newProps.derived_virtual_indices = virtual.indices;
newProps.derived_virtual_row_ids = R.pluck('id', virtual.data);
newProps.derived_virtual_row_ids = R.pluck('id', virtual.data as IndexedData);
}

if (!viewportCached) {
newProps.derived_viewport_data = viewport.data;
newProps.derived_viewport_indices = viewport.indices;
newProps.derived_viewport_row_ids = R.pluck('id', viewport.data);
newProps.derived_viewport_row_ids = R.pluck('id', viewport.data as IndexedData);
}

if (!virtualSelectedRowsCached) {
newProps.derived_virtual_selected_rows = virtual_selected_rows;
newProps.derived_virtual_selected_row_ids = R.map(
i => virtual.data[i].id,
i => (virtual.data as IndexedData)[i].id,
virtual_selected_rows
);
}
Expand All @@ -91,7 +92,7 @@ export default () => {
if (!viewportSelectedRowsCached) {
newProps.derived_viewport_selected_rows = viewport_selected_rows;
newProps.derived_viewport_selected_row_ids = R.map(
i => viewport.data[i].id,
i => (viewport.data as IndexedData)[i].id,
viewport_selected_rows
);
}
Expand Down
6 changes: 4 additions & 2 deletions src/dash-table/components/Table/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ export interface ICellCoordinates {
export type ColumnId = string;
export type Columns = IColumn[];
export type Data = Datum[];
export type Datum = IDatumObject | any;
export type IndexedData = IndexedDatum[];
export type Datum = IDatumObject;
export type IndexedDatum = Omit<Datum, 'id'> & { id: number | string; };
export type Indices = number[];
export type RowId = string | number;
export type SelectedCells = ICellCoordinates[];
Expand Down Expand Up @@ -209,7 +211,7 @@ export type IColumnType = INumberColumn | ITextColumn | IDatetimeColumn | IAnyCo
export type IColumn = IBaseColumn & IColumnType;

interface IDatumObject {
[key: string]: any;
[key: string]: boolean | number | string | null | undefined;
}

export interface IDropdownValue {
Expand Down
82 changes: 60 additions & 22 deletions src/dash-table/conditional/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ export interface IIndexedHeaderElement {
}

export interface IIndexedRowElement {
row_index?: number | 'odd' | 'even';
row_index?: number[] | number | 'odd' | 'even';
}

export interface INamedElement {
column_id?: ColumnId;
column_id?: ColumnId[] | ColumnId;
}

export interface IStateElement {
state?: 'active' | 'selected';
}

export interface ITypedElement {
Expand All @@ -29,17 +33,29 @@ export interface IEditableElement {
}

export type ConditionalBasicFilter = INamedElement & ITypedElement;
export type ConditionalDataCell = IConditionalElement & IIndexedRowElement & INamedElement & ITypedElement & IEditableElement;
export type ConditionalDataCell = IConditionalElement & IIndexedRowElement & INamedElement & IStateElement & ITypedElement & IEditableElement;
export type ConditionalCell = INamedElement & ITypedElement;
export type ConditionalHeader = IIndexedHeaderElement & INamedElement & ITypedElement;

function ifAstFilter(ast: QuerySyntaxTree, datum: Datum) {
return ast.isValid && ast.evaluate(datum);
}

export function ifColumnStateActive(condition: IStateElement | undefined, active: boolean) {
return condition?.state !== 'active' || active;
}

export function ifColumnStateSelected(condition: IStateElement | undefined, selected: boolean) {
return condition?.state !== 'selected' || selected;
}

export function ifColumnId(condition: INamedElement | undefined, columnId: ColumnId) {
return !condition ||
condition.column_id === undefined ||
if (!condition || condition.column_id === undefined) {
return true;
}

return Array.isArray(condition.column_id) ?
R.includes(columnId, condition.column_id) :
condition.column_id === columnId;
}

Expand All @@ -50,27 +66,29 @@ export function ifColumnType(condition: ITypedElement | undefined, columnType?:
}

export function ifRowIndex(condition: IIndexedRowElement | undefined, rowIndex: number) {
if (!condition ||
condition.row_index === undefined) {
if (!condition || condition.row_index === undefined) {
return true;
}

const rowCondition = condition.row_index;
return typeof rowCondition === 'number' ?
rowIndex === rowCondition :
rowCondition === 'odd' ? rowIndex % 2 === 1 : rowIndex % 2 === 0;
return typeof rowCondition === 'string' ?
rowIndex % 2 === (rowCondition === 'odd' ? 1 : 0) :
Array.isArray(rowCondition) ?
R.includes(rowIndex, rowCondition) :
rowIndex === rowCondition;
}

export function ifHeaderIndex(condition: IIndexedHeaderElement | undefined, headerIndex: number) {
if (!condition ||
condition.header_index === undefined) {
if (!condition || condition.header_index === undefined) {
return true;
}

const headerCondition = condition.header_index;
return typeof headerCondition === 'number' ?
headerIndex === headerCondition :
headerCondition === 'odd' ? headerIndex % 2 === 1 : headerIndex % 2 === 0;
return typeof headerCondition === 'string' ?
headerIndex % 2 === (headerCondition === 'odd' ? 1 : 0) :
Array.isArray(headerCondition) ?
R.includes(headerIndex, headerCondition) :
headerIndex === headerCondition;
}

export function ifFilter(condition: IConditionalElement | undefined, datum: Datum) {
Expand All @@ -89,32 +107,52 @@ export function ifEditable(condition: IEditableElement | undefined, isEditable:

export type Filter<T> = (s: T[]) => T[];

export const matchesDataCell = (datum: Datum, i: number, column: IColumn): Filter<IConvertedStyle> => R.filter<IConvertedStyle>((style =>
style.matchesRow(i) &&
export const matchesDataCell = (
datum: Datum,
i: number, column: IColumn,
active: boolean,
selected: boolean
): Filter<IConvertedStyle> => R.filter<IConvertedStyle>((style =>
!style.checksHeaderRow() &&
style.matchesActive(active) &&
style.matchesSelected(selected) &&
style.matchesDataRow(i) &&
style.matchesColumn(column) &&
style.matchesFilter(datum)
));

export const matchesFilterCell = (column: IColumn): Filter<IConvertedStyle> => R.filter<IConvertedStyle>((style =>
!style.checksState() &&
!style.checksDataRow() &&
!style.checksHeaderRow() &&
style.matchesColumn(column)
));

export const matchesHeaderCell = (i: number, column: IColumn): Filter<IConvertedStyle> => R.filter<IConvertedStyle>((style =>
style.matchesRow(i) &&
!style.checksState() &&
!style.checksDataRow() &&
style.matchesHeaderRow(i) &&
style.matchesColumn(column)
));

export const matchesDataOpCell = (datum: Datum, i: number): Filter<IConvertedStyle> => R.filter<IConvertedStyle>((style =>
!style.checksState() &&
!style.checksColumn() &&
style.matchesRow(i) &&
!style.checksHeaderRow() &&
style.matchesDataRow(i) &&
style.matchesFilter(datum)
));

export const getFilterOpStyles: Filter<IConvertedStyle> = R.filter<IConvertedStyle>((style =>
!style.checksState() &&
!style.checksDataRow() &&
!style.checksHeaderRow() &&
!style.checksColumn()
));

export const getHeaderOpStyles = (i: number): Filter<IConvertedStyle> => R.filter<IConvertedStyle>((style =>
style.matchesRow(i) &&
!style.checksColumn()
));
!style.checksDataRow() &&
!style.checksState() &&
!style.checksColumn() &&
style.matchesHeaderRow(i)
));
Loading

0 comments on commit 2ec0b43

Please sign in to comment.