Skip to content

Commit c25f578

Browse files
authored
feat: Column sources for ui.table formatting (#1010)
Fixes #984 Allows all string format values to instead specify a column as the source. Ensures the column is always fetched and throws if the column is not a string type (can cause some really bad performance issues if value formatting is invalid). Also modified to just resolve all theme colors since this change could lead to unresolved theme colors in a column source. The example in the docs or this example shows the feature working. Change the String cast on line 6 to non-string to test the error is thrown and displayed. This example uses an input table joined to the table so you can adjust the formatting colors via input table (something Raffi asked about specifically) ```py from deephaven import input_table from deephaven import ui import deephaven.plot.express as dx _stocks = dx.data.stocks() _source = _stocks.select_distinct("Sym").update("Color=(String)null") color_source = input_table(init_table=_source, key_cols="Sym") t = ui.table( _stocks.natural_join(color_source, "Sym", "SymColor=Color"), hidden_columns=["SymColor"], format_=[ ui.TableFormat(cols="Sym", background_color="SymColor") ], ) ```
1 parent 1343ec8 commit c25f578

File tree

9 files changed

+98
-20
lines changed

9 files changed

+98
-20
lines changed

.dockerignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,6 @@ test-results/
4848
playwright-report/
4949
blob-report/
5050
playwright/.cache/
51+
52+
# Ignore docs
53+
**/*.md

plugins/ui/docs/components/table.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,24 @@ t = ui.table(
6666
)
6767
```
6868

69+
### Formatting values from a column source
70+
71+
Any string value for a formatting rule can be read from a column by specifying the column name as the value. Note that if a value matches a column name, it will always be used (i.e., the theme color `positive` can not be used as a direct value if there is also a column called `positive`). The following example sets the `background_color` of column `x` using the value of the `bg_color` column.
72+
73+
```py
74+
from deephaven import ui
75+
76+
_t = empty_table(100).update(["x = i", "y = sin(i)", "bg_color = x % 2 == 0 ? `positive` : `negative`"])
77+
78+
t = ui.table(
79+
_t,
80+
format_=[
81+
ui.TableFormat(cols="x", background_color="bg_color"),
82+
],
83+
hidden_columns=["bg_color"],
84+
)
85+
```
86+
6987
### Formatting color
7088

7189
Formatting rules for colors support Deephaven theme colors, hex colors, or any valid CSS color (e.g., `red`, `#ff0000`, `rgb(255, 0, 0)`). It is **recommended to use Deephaven theme colors** when possible to maintain a consistent look and feel across the UI. Theme colors will also automatically update if the user changes the theme.

plugins/ui/src/js/src/elements/UITable/UITable.tsx

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
1+
import React, {
2+
useCallback,
3+
useEffect,
4+
useMemo,
5+
useRef,
6+
useState,
7+
} from 'react';
28
import { useSelector } from 'react-redux';
39
import classNames from 'classnames';
410
import {
@@ -10,6 +16,7 @@ import {
1016
IrisGridUtils,
1117
} from '@deephaven/iris-grid';
1218
import {
19+
ColorValues,
1320
colorValueStyle,
1421
resolveCssVariablesInRecord,
1522
useStyleProps,
@@ -39,9 +46,14 @@ const log = Log.module('@deephaven/js-plugin-ui/UITable');
3946
* @returns A stable array if none of the elements have changed
4047
*/
4148
function useStableArray<T>(array: T[]): T[] {
42-
// eslint-disable-next-line react-hooks/exhaustive-deps
43-
const stableArray = useMemo(() => array, [...array]);
44-
return stableArray;
49+
const stableArray = useRef<T[]>(array);
50+
if (
51+
array.length !== stableArray.current.length ||
52+
!array.every((v, i) => v === stableArray.current[i])
53+
) {
54+
stableArray.current = array;
55+
}
56+
return stableArray.current;
4557
}
4658

4759
export function UITable({
@@ -162,17 +174,10 @@ export function UITable({
162174
});
163175
});
164176

165-
format.forEach(rule => {
166-
const { color, background_color: backgroundColor } = rule;
167-
if (color != null) {
168-
colorSet.add(color);
169-
}
170-
if (backgroundColor != null) {
171-
colorSet.add(backgroundColor);
172-
}
173-
});
174-
175177
const colorRecord: Record<string, string> = {};
178+
ColorValues.forEach(c => {
179+
colorRecord[c] = colorValueStyle(c);
180+
});
176181
colorSet.forEach(c => {
177182
colorRecord[c] = colorValueStyle(c);
178183
});
@@ -183,7 +188,7 @@ export function UITable({
183188
newColorMap.set(key, value);
184189
});
185190
return newColorMap;
186-
}, [databars, format, theme]);
191+
}, [theme, databars]);
187192

188193
if (model) {
189194
model.setColorMap(colorMap);
@@ -256,11 +261,35 @@ export function UITable({
256261
};
257262
}, [databars, dh, exportedTable, layoutHints, format, columnDisplayNames]);
258263

264+
// Get any format values that match column names
265+
// Assume the format value is derived from the column
266+
const formatColumnSources = useMemo(() => {
267+
if (columns == null) {
268+
return [];
269+
}
270+
const columnSet = new Set(columns.map(column => column.name));
271+
const alwaysFetch: string[] = [];
272+
format.forEach(rule => {
273+
Object.entries(rule).forEach(([key, value]) => {
274+
if (
275+
key !== 'cols' &&
276+
key !== 'if_' &&
277+
typeof value === 'string' &&
278+
columnSet.has(value)
279+
) {
280+
alwaysFetch.push(value);
281+
}
282+
});
283+
});
284+
return alwaysFetch;
285+
}, [format, columns]);
286+
259287
const modelColumns = model?.columns ?? EMPTY_ARRAY;
260288

261-
const alwaysFetchColumnsArray = useStableArray(
262-
ensureArray(alwaysFetchColumnsProp)
263-
);
289+
const alwaysFetchColumnsArray = useStableArray([
290+
...ensureArray(alwaysFetchColumnsProp),
291+
...formatColumnSources,
292+
]);
264293

265294
const alwaysFetchColumns = useMemo(() => {
266295
if (alwaysFetchColumnsArray[0] === true) {

plugins/ui/src/js/src/elements/UITable/UITableModel.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,20 +439,39 @@ class UITableModel extends IrisGridModel {
439439
// eslint-disable-next-line no-continue
440440
continue;
441441
}
442+
443+
let resolvedFormatValue = formatValue;
444+
const columnSourceIndex =
445+
typeof formatValue === 'string'
446+
? this.getColumnIndexByName(formatValue)
447+
: null;
448+
if (columnSourceIndex != null) {
449+
const columnSource = this.columns[columnSourceIndex];
450+
if (!TableUtils.isStringType(columnSource.type)) {
451+
throw new Error(
452+
`Column ${columnSource.name} which provides TableFormat values for ${formatKey} is of type ${columnSource.type}. Columns that provide TableFormat values must be of type string.`
453+
);
454+
}
455+
resolvedFormatValue = this.valueForCell(
456+
columnSourceIndex,
457+
row
458+
) as NonNullable<FormattingRule[K]>;
459+
}
460+
442461
if (
443462
cols == null ||
444463
this.formatColumnMatch(ensureArray(cols), columnName)
445464
) {
446465
if (if_ == null) {
447-
return formatValue;
466+
return resolvedFormatValue;
448467
}
449468
const rowValues = this.model.row(row)?.data;
450469
if (rowValues == null) {
451470
return undefined;
452471
}
453472
const whereValue = rowValues.get(getFormatCustomColumnName(i))?.value;
454473
if (whereValue === true) {
455-
return formatValue;
474+
return resolvedFormatValue;
456475
}
457476
}
458477
}

tests/app.d/ui_table.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@
3434
],
3535
)
3636

37+
t_color_column_source = ui.table(
38+
_t.update("bg_color = x % 2 == 0 ? `positive` : `negative`"),
39+
format_=[
40+
ui.TableFormat(cols="x", background_color="bg_color"),
41+
],
42+
hidden_columns=["bg_color"],
43+
)
44+
3745
t_priority = ui.table(
3846
_t,
3947
format_=[

tests/ui_table.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ test.describe('UI flex components', () => {
99
't_alignment',
1010
't_background_color',
1111
't_color',
12+
't_color_column_source',
1213
't_priority',
1314
't_value_format',
1415
't_display_names',
15.4 KB
Loading
26.5 KB
Loading
16.2 KB
Loading

0 commit comments

Comments
 (0)