Repository: deephaven/deephaven-plugins (language: python)

Issue:
ui.table format values from a column
We should support reading the value for a format from a column. We could do this in a few ways

1. Check if all values are column names. If they are, add the column to `always_fetch_columns`
2. Add a `*_col` field for the values
3. Have a dataclass that has a key to indicate a value is a column. E.g. `color=ui.TableColumn("X")` and then the `TableColumn` dataclass has metadata indicating it's a column.


Discussion / hints:

Fix patch:
diff --git a/.dockerignore b/.dockerignore
index aa00b07c7..004d860e9 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -48,3 +48,6 @@ test-results/
 playwright-report/
 blob-report/
 playwright/.cache/
+
+# Ignore docs
+**/*.md
diff --git a/plugins/ui/docs/components/table.md b/plugins/ui/docs/components/table.md
index 7f01594cd..1895ad1d7 100644
--- a/plugins/ui/docs/components/table.md
+++ b/plugins/ui/docs/components/table.md
@@ -66,6 +66,24 @@ t = ui.table(
 )
 ```
 
+### Formatting values from a column source
+
+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.
+
+```py
+from deephaven import ui
+
+_t = empty_table(100).update(["x = i", "y = sin(i)", "bg_color = x % 2 == 0 ? `positive` : `negative`"])
+
+t = ui.table(
+    _t,
+    format_=[
+        ui.TableFormat(cols="x", background_color="bg_color"),
+    ],
+    hidden_columns=["bg_color"],
+)
+```
+
 ### Formatting color
 
 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.
diff --git a/plugins/ui/src/js/src/elements/UITable/UITable.tsx b/plugins/ui/src/js/src/elements/UITable/UITable.tsx
index e4fb07f1c..f0f29b211 100644
--- a/plugins/ui/src/js/src/elements/UITable/UITable.tsx
+++ b/plugins/ui/src/js/src/elements/UITable/UITable.tsx
@@ -1,4 +1,10 @@
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from 'react';
 import { useSelector } from 'react-redux';
 import classNames from 'classnames';
 import {
@@ -10,6 +16,7 @@ import {
   IrisGridUtils,
 } from '@deephaven/iris-grid';
 import {
+  ColorValues,
   colorValueStyle,
   resolveCssVariablesInRecord,
   useStyleProps,
@@ -39,9 +46,14 @@ const log = Log.module('@deephaven/js-plugin-ui/UITable');
  * @returns A stable array if none of the elements have changed
  */
 function useStableArray<T>(array: T[]): T[] {
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  const stableArray = useMemo(() => array, [...array]);
-  return stableArray;
+  const stableArray = useRef<T[]>(array);
+  if (
+    array.length !== stableArray.current.length ||
+    !array.every((v, i) => v === stableArray.current[i])
+  ) {
+    stableArray.current = array;
+  }
+  return stableArray.current;
 }
 
 export function UITable({
@@ -162,17 +174,10 @@ export function UITable({
       });
     });
 
-    format.forEach(rule => {
-      const { color, background_color: backgroundColor } = rule;
-      if (color != null) {
-        colorSet.add(color);
-      }
-      if (backgroundColor != null) {
-        colorSet.add(backgroundColor);
-      }
-    });
-
     const colorRecord: Record<string, string> = {};
+    ColorValues.forEach(c => {
+      colorRecord[c] = colorValueStyle(c);
+    });
     colorSet.forEach(c => {
       colorRecord[c] = colorValueStyle(c);
     });
@@ -183,7 +188,7 @@ export function UITable({
       newColorMap.set(key, value);
     });
     return newColorMap;
-  }, [databars, format, theme]);
+  }, [theme, databars]);
 
   if (model) {
     model.setColorMap(colorMap);
@@ -256,11 +261,35 @@ export function UITable({
     };
   }, [databars, dh, exportedTable, layoutHints, format, columnDisplayNames]);
 
+  // Get any format values that match column names
+  // Assume the format value is derived from the column
+  const formatColumnSources = useMemo(() => {
+    if (columns == null) {
+      return [];
+    }
+    const columnSet = new Set(columns.map(column => column.name));
+    const alwaysFetch: string[] = [];
+    format.forEach(rule => {
+      Object.entries(rule).forEach(([key, value]) => {
+        if (
+          key !== 'cols' &&
+          key !== 'if_' &&
+          typeof value === 'string' &&
+          columnSet.has(value)
+        ) {
+          alwaysFetch.push(value);
+        }
+      });
+    });
+    return alwaysFetch;
+  }, [format, columns]);
+
   const modelColumns = model?.columns ?? EMPTY_ARRAY;
 
-  const alwaysFetchColumnsArray = useStableArray(
-    ensureArray(alwaysFetchColumnsProp)
-  );
+  const alwaysFetchColumnsArray = useStableArray([
+    ...ensureArray(alwaysFetchColumnsProp),
+    ...formatColumnSources,
+  ]);
 
   const alwaysFetchColumns = useMemo(() => {
     if (alwaysFetchColumnsArray[0] === true) {
diff --git a/plugins/ui/src/js/src/elements/UITable/UITableModel.ts b/plugins/ui/src/js/src/elements/UITable/UITableModel.ts
index 1082e8aee..11576c838 100644
--- a/plugins/ui/src/js/src/elements/UITable/UITableModel.ts
+++ b/plugins/ui/src/js/src/elements/UITable/UITableModel.ts
@@ -439,12 +439,31 @@ class UITableModel extends IrisGridModel {
         // eslint-disable-next-line no-continue
         continue;
       }
+
+      let resolvedFormatValue = formatValue;
+      const columnSourceIndex =
+        typeof formatValue === 'string'
+          ? this.getColumnIndexByName(formatValue)
+          : null;
+      if (columnSourceIndex != null) {
+        const columnSource = this.columns[columnSourceIndex];
+        if (!TableUtils.isStringType(columnSource.type)) {
+          throw new Error(
+            `Column ${columnSource.name} which provides TableFormat values for ${formatKey} is of type ${columnSource.type}. Columns that provide TableFormat values must be of type string.`
+          );
+        }
+        resolvedFormatValue = this.valueForCell(
+          columnSourceIndex,
+          row
+        ) as NonNullable<FormattingRule[K]>;
+      }
+
       if (
         cols == null ||
         this.formatColumnMatch(ensureArray(cols), columnName)
       ) {
         if (if_ == null) {
-          return formatValue;
+          return resolvedFormatValue;
         }
         const rowValues = this.model.row(row)?.data;
         if (rowValues == null) {
@@ -452,7 +471,7 @@ class UITableModel extends IrisGridModel {
         }
         const whereValue = rowValues.get(getFormatCustomColumnName(i))?.value;
         if (whereValue === true) {
-          return formatValue;
+          return resolvedFormatValue;
         }
       }
     }


Test patch:
diff --git a/tests/app.d/ui_table.py b/tests/app.d/ui_table.py
index af3a8c273..d4339aa2b 100644
--- a/tests/app.d/ui_table.py
+++ b/tests/app.d/ui_table.py
@@ -34,6 +34,14 @@
     ],
 )
 
+t_color_column_source = ui.table(
+    _t.update("bg_color = x % 2 == 0 ? `positive` : `negative`"),
+    format_=[
+        ui.TableFormat(cols="x", background_color="bg_color"),
+    ],
+    hidden_columns=["bg_color"],
+)
+
 t_priority = ui.table(
     _t,
     format_=[
diff --git a/tests/ui_table.spec.ts b/tests/ui_table.spec.ts
index 360207111..9815f2e8e 100644
--- a/tests/ui_table.spec.ts
+++ b/tests/ui_table.spec.ts
@@ -9,6 +9,7 @@ test.describe('UI flex components', () => {
     't_alignment',
     't_background_color',
     't_color',
+    't_color_column_source',
     't_priority',
     't_value_format',
     't_display_names',
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-column-source-1-chromium-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-column-source-1-chromium-linux.png
new file mode 100644
index 000000000..913beeee8
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-column-source-1-chromium-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-column-source-1-firefox-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-column-source-1-firefox-linux.png
new file mode 100644
index 000000000..0e32d07f0
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-column-source-1-firefox-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-column-source-1-webkit-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-column-source-1-webkit-linux.png
new file mode 100644
index 000000000..132482e6c
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-column-source-1-webkit-linux.png differ