Table: Color text, color background, and apply to row can co-mingle (#109939)

* Table: Color text, color background, and apply to row can co-mingle

* fix test

* lean on existing memoization and utils more

* just make that method a prop of TableCellRenderer

* add prop to tooltip stuff as well

* fix another test

* update TableNG with apply to row mixed color cell table

* simplify color overrides table

* special case: apply to row transparent bg

* add unit test

* delete erroneous import

* update for readability
This commit is contained in:
Paul Marbach
2025-08-25 19:47:11 +00:00
committed by GitHub
parent 4d2decfa0c
commit 67502a5a7c
10 changed files with 444 additions and 94 deletions
@@ -434,9 +434,6 @@
"id": 1,
"options": {
"cellHeight": "sm",
"frozenColumns": {
"left": 1
},
"footer": {
"countRows": false,
"enablePagination": false,
@@ -445,6 +442,9 @@
"show": true
},
"frameIndex": 0,
"frozenColumns": {
"left": 1
},
"showHeader": true,
"sortBy": [
{
@@ -797,6 +797,10 @@
"value": {
"type": "data-links"
}
},
{
"id": "custom.width",
"value": 140
}
]
}
@@ -804,7 +808,7 @@
},
"gridPos": {
"h": 7,
"w": 24,
"w": 12,
"x": 0,
"y": 12
},
@@ -834,6 +838,166 @@
"title": "Colors and Links",
"type": "table"
},
{
"datasource": {
"type": "grafana-testdata-datasource"
},
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "transparent",
"mode": "fixed"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false,
"width": 100,
"wrapHeaderText": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "highlight"
},
"properties": [
{
"id": "custom.cellOptions",
"value": {
"applyToRow": true,
"mode": "basic",
"type": "color-background"
}
},
{
"id": "mappings",
"value": [
{
"options": {
"1": {
"color": "#fff899",
"index": 0
}
},
"type": "value"
}
]
}
]
},
{
"matcher": {
"id": "byRegexp",
"options": "/^color-*/"
},
"properties": [
{
"id": "mappings",
"value": [
{
"options": {
"1": {
"color": "green",
"index": 0
}
},
"type": "value"
}
]
}
]
},
{
"matcher": {
"id": "byName",
"options": "color-bg"
},
"properties": [
{
"id": "custom.cellOptions",
"value": {
"type": "color-background"
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "color-text"
},
"properties": [
{
"id": "custom.cellOptions",
"value": {
"type": "color-text"
}
},
{
"id": "color",
"value": {
"fixedColor": "text",
"mode": "fixed"
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "use-case"
},
"properties": [
{
"id": "custom.width"
}
]
}
]
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 12
},
"id": 10,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": ["sum"],
"show": false
},
"showHeader": true,
"sortBy": []
},
"pluginVersion": "12.2.0-pre",
"targets": [
{
"csvContent": "use-case,normal,color-text,color-bg,highlight\n\"color bg and apply to row\",1,0,1,1\n\"color text and apply to row\",1,1,0,1\n\"color text + color bg + apply to row\",1,1,1,1\n\"only apply to row\",1,0,0,1\n\"only color bg\",1,0,1,0\n\"only color text\",1,1,0,0\n\"color text + color bg\",1,1,1,0\n\"no colorization\",1,0,0,0",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Apply to Row - mixed color cell types",
"type": "table"
},
{
"datasource": {
"type": "grafana-testdata-datasource"
@@ -1697,5 +1861,5 @@
"timezone": "",
"title": "Panel Tests - Table - Kitchen Sink",
"uid": "dcb9f5e9-8066-4397-889e-864b99555dbb",
"version": 9
"version": 32
}
@@ -2,6 +2,8 @@ import { render, RenderResult } from '@testing-library/react';
import { Field, FieldType, MappingType, createTheme } from '@grafana/data';
import { getTextColorForBackground } from '../../../../utils/colors';
import { PillCell } from './PillCell';
describe('PillCell', () => {
@@ -26,49 +28,94 @@ describe('PillCell', () => {
describe('Color by hash (classic palette)', () => {
it('single value', () => {
expectHTML(
render(<PillCell field={fieldWithValues(['value1'])} rowIdx={0} theme={theme} />),
`<span style="background-color: rgb(63, 43, 91); color: rgb(255, 255, 255);">value1</span>`
render(
<PillCell
getTextColorForBackground={getTextColorForBackground}
field={fieldWithValues(['value1'])}
rowIdx={0}
theme={theme}
/>
),
`<span style="background-color: rgb(63, 43, 91); color: rgb(247, 248, 250);">value1</span>`
);
});
it('empty string', () => {
expectHTML(render(<PillCell field={fieldWithValues([''])} rowIdx={0} theme={theme} />), '');
expectHTML(
render(
<PillCell
getTextColorForBackground={getTextColorForBackground}
field={fieldWithValues([''])}
rowIdx={0}
theme={theme}
/>
),
''
);
});
it('null', () => {
const { container } = render(<PillCell field={fieldWithValues([])} rowIdx={0} theme={theme} />);
const { container } = render(
<PillCell
getTextColorForBackground={getTextColorForBackground}
field={fieldWithValues([])}
rowIdx={0}
theme={theme}
/>
);
expect(container).toBeEmptyDOMElement();
});
it('CSV values', () => {
expectHTML(
render(<PillCell field={fieldWithValues(['value1,value2,value3'])} rowIdx={0} theme={theme} />),
render(
<PillCell
getTextColorForBackground={getTextColorForBackground}
field={fieldWithValues(['value1,value2,value3'])}
rowIdx={0}
theme={theme}
/>
),
`
<span style="background-color: rgb(63, 43, 91); color: rgb(255, 255, 255);">value1</span>
<span style="background-color: rgb(252, 226, 222); color: rgb(0, 0, 0);">value2</span>
<span style="background-color: rgb(81, 149, 206); color: rgb(0, 0, 0);">value3</span>
<span style="background-color: rgb(63, 43, 91); color: rgb(247, 248, 250);">value1</span>
<span style="background-color: rgb(252, 226, 222); color: rgb(32, 34, 38);">value2</span>
<span style="background-color: rgb(81, 149, 206); color: rgb(247, 248, 250);">value3</span>
`
);
});
it('JSON array values', () => {
expectHTML(
render(<PillCell field={fieldWithValues(['["value1","value2","value3"]'])} rowIdx={0} theme={theme} />),
render(
<PillCell
getTextColorForBackground={getTextColorForBackground}
field={fieldWithValues(['["value1","value2","value3"]'])}
rowIdx={0}
theme={theme}
/>
),
`
<span style="background-color: rgb(63, 43, 91); color: rgb(255, 255, 255);">value1</span>
<span style="background-color: rgb(252, 226, 222); color: rgb(0, 0, 0);">value2</span>
<span style="background-color: rgb(81, 149, 206); color: rgb(0, 0, 0);">value3</span>
<span style="background-color: rgb(63, 43, 91); color: rgb(247, 248, 250);">value1</span>
<span style="background-color: rgb(252, 226, 222); color: rgb(32, 34, 38);">value2</span>
<span style="background-color: rgb(81, 149, 206); color: rgb(247, 248, 250);">value3</span>
`
);
});
it('non-string values', () => {
expectHTML(
render(<PillCell field={fieldWithValues(['[100,200,300]'])} rowIdx={0} theme={theme} />),
render(
<PillCell
getTextColorForBackground={getTextColorForBackground}
field={fieldWithValues(['[100,200,300]'])}
rowIdx={0}
theme={theme}
/>
),
`
<span style="background-color: rgb(252, 226, 222); color: rgb(0, 0, 0);">100</span>
<span style="background-color: rgb(222, 218, 247); color: rgb(0, 0, 0);">200</span>
<span style="background-color: rgb(249, 217, 249); color: rgb(0, 0, 0);">300</span>
<span style="background-color: rgb(252, 226, 222); color: rgb(32, 34, 38);">100</span>
<span style="background-color: rgb(222, 218, 247); color: rgb(32, 34, 38);">200</span>
<span style="background-color: rgb(249, 217, 249); color: rgb(32, 34, 38);">300</span>
`
);
});
@@ -107,12 +154,14 @@ describe('PillCell', () => {
} satisfies Field;
expectHTML(
render(<PillCell field={field} rowIdx={0} theme={theme} />),
render(
<PillCell getTextColorForBackground={getTextColorForBackground} field={field} rowIdx={0} theme={theme} />
),
`
<span style="background-color: rgb(0, 255, 0); color: rgb(0, 0, 0);">success</span>
<span style="background-color: rgb(255, 0, 0); color: rgb(0, 0, 0);">error</span>
<span style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">warning</span>
<span style="background-color: rgb(255, 120, 10); color: rgb(0, 0, 0);">unknown</span>
<span style="background-color: rgb(0, 255, 0); color: rgb(247, 248, 250);">success</span>
<span style="background-color: rgb(255, 0, 0); color: rgb(247, 248, 250);">error</span>
<span style="background-color: rgb(255, 255, 0); color: rgb(32, 34, 38);">warning</span>
<span style="background-color: rgb(255, 120, 10); color: rgb(247, 248, 250);">unknown</span>
`
);
});
@@ -4,7 +4,6 @@ import { useMemo } from 'react';
import {
GrafanaTheme2,
classicColors,
colorManipulator,
Field,
getColorByStringHash,
FALLBACK_COLOR,
@@ -14,12 +13,23 @@ import { FieldColorModeId } from '@grafana/schema';
import { PillCellProps, TableCellStyles, TableCellValue } from '../types';
export function PillCell({ rowIdx, field, theme }: PillCellProps) {
export function PillCell({ rowIdx, field, theme, getTextColorForBackground }: PillCellProps) {
const value = field.values[rowIdx];
const pills: Pill[] = useMemo(() => {
const pillValues = inferPills(value);
return pillValues.length > 0 ? createPills(pillValues, field, theme) : [];
}, [value, field, theme]);
return pillValues.length > 0
? pillValues.map((pill, index) => {
const bgColor = getPillColor(pill, field, theme);
const textColor = getTextColorForBackground(bgColor);
return {
value: String(pill),
key: `${pill}-${index}`,
bgColor,
color: textColor,
};
})
: [];
}, [value, field, theme, getTextColorForBackground]);
if (pills.length === 0) {
return null;
@@ -49,19 +59,6 @@ interface Pill {
const SPLIT_RE = /\s*,\s*/;
const TRANSPARENT = 'rgba(0,0,0,0)';
function createPills(pillValues: unknown[], field: Field, theme: GrafanaTheme2): Pill[] {
return pillValues.map((pill, index) => {
const bgColor = getPillColor(pill, field, theme);
const textColor = colorManipulator.getContrastRatio('#FFFFFF', bgColor) >= 4.5 ? '#FFFFFF' : '#000000';
return {
value: String(pill),
key: `${pill}-${index}`,
bgColor,
color: textColor,
};
});
}
export function inferPills(rawValue: TableCellValue): unknown[] {
if (rawValue === '' || rawValue == null) {
return [];
@@ -91,6 +91,7 @@ describe('TableNG Cells renderers', () => {
getActions={jest.fn(() => [
{ title: 'Action', onClick: jest.fn(() => {}), confirmation: jest.fn(), style: {} },
])}
getTextColorForBackground={jest.fn(() => '#000000')}
/>
);
};
@@ -109,6 +110,7 @@ describe('TableNG Cells renderers', () => {
height={100}
width={100}
theme={createTheme()}
getTextColorForBackground={jest.fn(() => '#000000')}
cellOptions={cellOptions}
cellInspect={false}
showFilters={false}
@@ -110,7 +110,12 @@ const CELL_REGISTRY: Record<TableCellOptions['type'], CellRegistryEntry> = {
[TableCellDisplayMode.Pill]: {
// eslint-disable-next-line react/display-name
renderer: memo((props: TableCellRendererProps) => (
<PillCell rowIdx={props.rowIdx} field={props.field} theme={props.theme} />
<PillCell
rowIdx={props.rowIdx}
field={props.field}
theme={props.theme}
getTextColorForBackground={props.getTextColorForBackground}
/>
)),
getStyles: getPillStyles,
testField: (field: Field) => field.type === FieldType.string,
@@ -1,6 +1,7 @@
import 'react-data-grid/lib/styles.css';
import { clsx } from 'clsx';
import memoize from 'micro-memoize';
import { CSSProperties, Key, ReactNode, useCallback, useMemo, useRef, useState } from 'react';
import {
Cell,
@@ -28,6 +29,7 @@ import { Trans } from '@grafana/i18n';
import { FieldColorModeId, TableCellTooltipPlacement } from '@grafana/schema';
import { useStyles2, useTheme2 } from '../../../themes/ThemeContext';
import { getTextColorForBackground as _getTextColorForBackground } from '../../../utils/colors';
import { Pagination } from '../../Pagination/Pagination';
import { PanelContext, usePanelContext } from '../../PanelChrome';
import { DataLinksActionsTooltip } from '../DataLinksActionsTooltip';
@@ -80,7 +82,7 @@ import {
frameToRecords,
getAlignment,
getApplyToRowBgFn,
getCellColorInlineStyles,
getCellColorInlineStylesFactory,
getCellLinks,
getCellOptions,
getDefaultRowHeight,
@@ -143,6 +145,7 @@ export function TableNG(props: TableNGProps) {
const rows = useMemo(() => frameToRecords(data), [data]);
const hasNestedFrames = useMemo(() => getIsNestedTable(data.fields), [data]);
const getTextColorForBackground = useMemo(() => memoize(_getTextColorForBackground, { maxSize: 1000 }), []);
const {
rows: filteredRows,
@@ -174,6 +177,11 @@ export function TableNG(props: TableNGProps) {
() => (hasNestedFrames ? width - COLUMN.EXPANDER_WIDTH : width) - scrollbarWidth,
[width, hasNestedFrames, scrollbarWidth]
);
const getCellColorInlineStyles = useMemo(() => getCellColorInlineStylesFactory(theme), [theme]);
const applyToRowBgFn = useMemo(
() => getApplyToRowBgFn(data.fields, getCellColorInlineStyles) ?? undefined,
[data.fields, getCellColorInlineStyles]
);
const typographyCtx = useMemo(
() =>
createTypographyContext(
@@ -226,7 +234,6 @@ export function TableNG(props: TableNGProps) {
footerOptions,
isCountRowsSet,
});
const applyToRowBgFn = useMemo(() => getApplyToRowBgFn(data.fields, theme) ?? undefined, [data.fields, theme]);
// normalize the row height into a function which returns a number, so we avoid a bunch of conditionals during rendering.
const rowHeightFn = useMemo((): ((row: TableRow) => number) => {
@@ -438,14 +445,12 @@ export function TableNG(props: TableNGProps) {
}
}
let style: CSSProperties | undefined;
if (rowCellStyle.color != null || rowCellStyle.background != null) {
style = rowCellStyle;
} else if (canBeColorized) {
let style: CSSProperties = { ...rowCellStyle };
if (canBeColorized) {
const value = props.row[props.column.key];
const displayValue = field.display!(value); // this fires here to get colors, then again to get rendered value?
style = getCellColorInlineStyles(theme, cellOptions, displayValue);
const cellColorStyles = getCellColorInlineStyles(cellOptions, displayValue, applyToRowBgFn != null);
Object.assign(style, cellColorStyles);
}
return (
@@ -486,6 +491,7 @@ export function TableNG(props: TableNGProps) {
showFilters={showFilters}
getActions={getCellActions}
disableSanitizeHtml={disableSanitizeHtml}
getTextColorForBackground={getTextColorForBackground}
/>
{showActions && (
<TableCellActions
@@ -549,6 +555,7 @@ export function TableNG(props: TableNGProps) {
disableSanitizeHtml,
field: tooltipField,
getActions: getCellActions,
getTextColorForBackground,
gridRef,
placement,
renderer: tooltipFieldRenderer,
@@ -560,10 +567,15 @@ export function TableNG(props: TableNGProps) {
renderCellContent = (props: RenderCellProps<TableRow, TableSummaryRow>): JSX.Element => {
// cached so we don't care about multiple calls.
const tooltipHeight = rowHeightFn(props.row);
let tooltipStyle: CSSProperties | undefined;
let tooltipStyle: CSSProperties = { ...rowCellStyle };
if (tooltipCanBeColorized) {
const tooltipDisplayValue = tooltipField.display!(props.row[tooltipDisplayName]); // this is yet another call to field.display() for the tooltip field
tooltipStyle = getCellColorInlineStyles(theme, tooltipCellOptions, tooltipDisplayValue);
const tooltipDisplayValue = tooltipField.display!(props.row[tooltipDisplayName]);
const tooltipCellColorStyles = getCellColorInlineStyles(
tooltipCellOptions,
tooltipDisplayValue,
applyToRowBgFn != null
);
Object.assign(tooltipStyle, tooltipCellColorStyles);
}
return (
@@ -624,6 +636,8 @@ export function TableNG(props: TableNGProps) {
footerCalcs,
frozenColumns,
getCellActions,
getCellColorInlineStyles,
getTextColorForBackground,
isCountRowsSet,
numFrozenColsFullyInView,
onCellFilterAdded,
@@ -19,6 +19,7 @@ export interface TableCellTooltipProps {
disableSanitizeHtml?: boolean;
field: Field;
getActions: (field: Field, rowIdx: number) => ActionModel[];
getTextColorForBackground: (bgColor: string) => string;
gridRef: RefObject<DataGridHandle>;
height: number;
placement?: TableCellTooltipPlacement;
@@ -40,6 +41,7 @@ export const TableCellTooltip = memo(
disableSanitizeHtml,
field,
getActions,
getTextColorForBackground,
gridRef,
height,
placement,
@@ -100,6 +102,7 @@ export const TableCellTooltip = memo(
field,
frame: data,
getActions,
getTextColorForBackground,
height,
rowIdx,
showFilters: false,
@@ -107,7 +110,19 @@ export const TableCellTooltip = memo(
value: rawValue,
width,
}) satisfies TableCellRendererProps,
[cellOptions, data, disableSanitizeHtml, field, getActions, height, rawValue, rowIdx, theme, width]
[
cellOptions,
data,
disableSanitizeHtml,
field,
getActions,
getTextColorForBackground,
height,
rawValue,
rowIdx,
theme,
width,
]
);
const cellElement = tooltipCaretRef.current?.closest<HTMLElement>('.rdg-cell');
@@ -167,6 +167,7 @@ export interface TableCellRendererProps {
showFilters: boolean;
getActions?: GetActionsFunctionLocal;
disableSanitizeHtml?: boolean;
getTextColorForBackground: (color: string) => string;
}
export type InspectCellProps = {
@@ -250,6 +251,7 @@ export interface PillCellProps {
theme: GrafanaTheme2;
field: Field;
rowIdx: number;
getTextColorForBackground: (color: string) => string;
}
export interface TableCellStyleOptions {
@@ -23,7 +23,7 @@ import {
extractPixelValue,
frameToRecords,
getAlignmentFactor,
getCellColorInlineStyles,
getCellColorInlineStylesFactory,
getCellLinks,
getCellOptions,
getComparator,
@@ -107,28 +107,104 @@ describe('TableNG utils', () => {
},
} as unknown as GrafanaTheme2;
it('should handle color background mode', () => {
const field = { type: TableCellDisplayMode.ColorBackground as const, mode: TableCellBackgroundDisplayMode.Basic };
it('should handle color text cell type', () => {
const cellOptions = {
type: TableCellDisplayMode.ColorText as const,
};
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
const colors = getCellColorInlineStyles(theme, field, displayValue);
expect(colors.background).toBe('rgb(255, 0, 0)');
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
const colors = getCellColorInlineStyles(cellOptions, displayValue, false);
expect(colors.color).toBe('#ff0000');
expect(colors).not.toHaveProperty('background');
});
it('should pass thru color background cell type in basic mode', () => {
const cellOptions = {
type: TableCellDisplayMode.ColorBackground as const,
mode: TableCellBackgroundDisplayMode.Basic,
};
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
const colors = getCellColorInlineStyles(cellOptions, displayValue, false);
expect(colors.background).toBe('#ff0000');
expect(colors.color).toBe('rgb(247, 248, 250)');
});
it('should handle color background gradient mode', () => {
const field = {
it('should handle color background cell type in gradient mode', () => {
const cellOptions = {
type: TableCellDisplayMode.ColorBackground as const,
mode: TableCellBackgroundDisplayMode.Gradient,
};
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
const colors = getCellColorInlineStyles(theme, field, displayValue);
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
const colors = getCellColorInlineStyles(cellOptions, displayValue, false);
expect(colors.background).toBe('linear-gradient(120deg, rgb(255, 54, 36), #ff0000)');
expect(colors.color).toBe('rgb(247, 248, 250)');
});
it('does not set CSSProperties for un-mapped cell types', () => {
const cellOptions = { type: TableCellDisplayMode.JSONView as const };
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
const colors = getCellColorInlineStyles(cellOptions, displayValue, false);
expect(colors).toEqual({});
});
describe('applyToRow', () => {
it.each([
['hex', '#ffffff00'],
['rgba', 'rgba(255,255,255,0)'],
['hsla', 'hsla(0,100%,100%,0)'],
])(
'should not apply background color if the display value is transparent (%s) and applyToRow is on',
(_format, colorDisplayValue) => {
const cellOptions = {
type: TableCellDisplayMode.ColorBackground as const,
mode: TableCellBackgroundDisplayMode.Basic,
};
const displayValue = { text: '100', numeric: 100, color: colorDisplayValue };
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
const colors = getCellColorInlineStyles(cellOptions, displayValue, true);
expect(colors).toEqual({});
}
);
it.each([
['hex', '#ffffff00'],
['rgba', 'rgba(255,255,255,0)'],
['hsla', 'hsla(0,100%,100%,0)'],
])(
'should apply background color if the display value is transparent (%s) and applyToRow is off',
(_format, colorDisplayValue) => {
const cellOptions = {
type: TableCellDisplayMode.ColorBackground as const,
mode: TableCellBackgroundDisplayMode.Basic,
};
const displayValue = { text: '100', numeric: 100, color: colorDisplayValue };
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
const colors = getCellColorInlineStyles(cellOptions, displayValue, false);
expect(colors).toEqual({
background: colorDisplayValue,
color: 'rgb(32, 34, 38)',
});
}
);
});
});
describe('frame to records conversion', () => {
@@ -1,4 +1,5 @@
import { Property } from 'csstype';
import memoize from 'micro-memoize';
import { CSSProperties } from 'react';
import { SortColumn } from 'react-data-grid';
import tinycolor from 'tinycolor2';
@@ -499,36 +500,58 @@ const CELL_GRADIENT_HUE_ROTATION_DEGREES = 5;
* @internal
* Returns the text and background colors for a table cell based on its options and display value.
*/
export function getCellColorInlineStyles(
theme: GrafanaTheme2,
cellOptions: TableCellOptions,
displayValue: DisplayValue
): CSSProperties {
// How much to darken elements depends upon if we're in dark mode
const darkeningFactor = theme.isDark ? 1 : -0.7;
// Setup color variables
let textColor: string | undefined = undefined;
let bgColor: string | undefined = undefined;
if (cellOptions.type === TableCellDisplayMode.ColorText) {
textColor = displayValue.color;
} else if (cellOptions.type === TableCellDisplayMode.ColorBackground) {
const mode = cellOptions.mode ?? TableCellBackgroundDisplayMode.Gradient;
if (mode === TableCellBackgroundDisplayMode.Basic) {
textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark);
bgColor = tinycolor(displayValue.color).toRgbString();
} else if (mode === TableCellBackgroundDisplayMode.Gradient) {
const bgColor2 = tinycolor(displayValue.color)
export function getCellColorInlineStylesFactory(theme: GrafanaTheme2) {
const bgCellTextColor = memoize((color: string) => getTextColorForAlphaBackground(color, theme.isDark), {
maxSize: 1000,
});
const darkeningFactor = theme.isDark ? 1 : -0.7; // How much to darken elements depends upon if we're in dark mode
const gradientBg = memoize(
(color: string) =>
tinycolor(color)
.darken(CELL_COLOR_DARKENING_MULTIPLIER * darkeningFactor)
.spin(CELL_GRADIENT_HUE_ROTATION_DEGREES);
textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark);
bgColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${displayValue.color})`;
}
}
.spin(CELL_GRADIENT_HUE_ROTATION_DEGREES)
.toRgbString(),
{ maxSize: 1000 }
);
const isTransparent = memoize(
(color: string) => {
// if hex, do the simple thing.
if (color[0] === '#') {
return color.length === 9 && color.endsWith('00');
}
// if not hex, just use tinycolor to avoid extra logic.
return tinycolor(color).getAlpha() === 0;
},
{ maxSize: 1000 }
);
return { color: textColor, background: bgColor };
return (cellOptions: TableCellOptions, displayValue: DisplayValue, hasApplyToRow: boolean): CSSProperties => {
const result: CSSProperties = {};
const displayValueColor = displayValue.color;
if (!displayValueColor) {
return result;
}
if (cellOptions.type === TableCellDisplayMode.ColorText) {
result.color = displayValueColor;
} else if (cellOptions.type === TableCellDisplayMode.ColorBackground) {
// return without setting anything if the bg is transparent. this allows
// the cell to inherit the row bg color if `applyToRow` is set.
if (hasApplyToRow && isTransparent(displayValueColor)) {
return result;
}
const mode = cellOptions.mode ?? TableCellBackgroundDisplayMode.Gradient;
result.color = bgCellTextColor(displayValueColor);
result.background =
mode === TableCellBackgroundDisplayMode.Gradient
? `linear-gradient(120deg, ${gradientBg(displayValueColor)}, ${displayValueColor})`
: displayValueColor;
}
return result;
};
}
/**
@@ -881,7 +904,10 @@ export function computeColWidths(fields: Field[], availWidth: number) {
* @internal
* if applyToRow is true in any field, return a function that gets the row background color
*/
export function getApplyToRowBgFn(fields: Field[], theme: GrafanaTheme2): ((rowIndex: number) => CSSProperties) | void {
export function getApplyToRowBgFn(
fields: Field[],
getCellColorInlineStyles: ReturnType<typeof getCellColorInlineStylesFactory>
): ((rowIndex: number) => CSSProperties) | void {
for (const field of fields) {
const cellOptions = getCellOptions(field);
const fieldDisplay = field.display;
@@ -890,7 +916,7 @@ export function getApplyToRowBgFn(fields: Field[], theme: GrafanaTheme2): ((rowI
cellOptions.type === TableCellDisplayMode.ColorBackground &&
cellOptions.applyToRow === true
) {
return (rowIndex: number) => getCellColorInlineStyles(theme, cellOptions, fieldDisplay(field.values[rowIndex]));
return (rowIndex: number) => getCellColorInlineStyles(cellOptions, fieldDisplay(field.values[rowIndex]), true);
}
}
}