diff --git a/packages/components/src/theme/ThemeUtils.test.ts b/packages/components/src/theme/ThemeUtils.test.ts index cef18d4103..3f88908032 100644 --- a/packages/components/src/theme/ThemeUtils.test.ts +++ b/packages/components/src/theme/ThemeUtils.test.ts @@ -558,9 +558,10 @@ describe.each([undefined, document.createElement('div')])( bbb: 'bbb', }; + jest.spyOn(window.CSS, 'supports').mockReturnValue(false); + const actual = resolveCssVariablesInRecord(given, targetElement); - expect(computedStyle.getPropertyValue).not.toHaveBeenCalled(); expect(ColorUtils.normalizeCssColor).not.toHaveBeenCalled(); expect(actual).toEqual(given); }); diff --git a/packages/components/src/theme/ThemeUtils.ts b/packages/components/src/theme/ThemeUtils.ts index 152cbf1dff..22cd1ba35c 100644 --- a/packages/components/src/theme/ThemeUtils.ts +++ b/packages/components/src/theme/ThemeUtils.ts @@ -320,21 +320,19 @@ export function resolveCssVariablesInRecord>( const result = {} as T; recordArray.forEach(([key, value], i) => { - // only resolve if it contains a css var expression - if (!value.includes(CSS_VAR_EXPRESSION_PREFIX)) { - (result as Record)[key] = value; - return; - } // resolves any variables in the expression let resolved = tempPropElComputedStyle.getPropertyValue( `--${TMP_CSS_PROP_PREFIX}-${i}` ); + + const containsCssVar = value.includes(CSS_VAR_EXPRESSION_PREFIX); + const isColor = CSS.supports('color', resolved); + if ( - // skip if resolved is already hex - !/^#[0-9A-F]{6}[0-9a-f]{0,2}$/i.test(resolved) && - // only try to normalize things that are valid colors + // only try to normalize non-hex strings that are valid colors // otherwise non-colors will be made #00000000 - CSS.supports('color', resolved) + isColor && + !/^#[0-9A-F]{6}[0-9a-f]{0,2}$/i.test(resolved) ) { // getting the computed background color is necessary // because resolved can still contain a color-mix() function @@ -344,7 +342,8 @@ export function resolveCssVariablesInRecord>( // convert color to hex, which is what monaco and plotly require resolved = ColorUtils.normalizeCssColor(color, isAlphaOptional); } - (result as Record)[key] = resolved; + (result as Record)[key] = + containsCssVar || isColor ? resolved : value; }); // Remove the temporary div diff --git a/packages/components/src/theme/colorUtils.ts b/packages/components/src/theme/colorUtils.ts index 8d32a22f98..9f8f5bbed4 100644 --- a/packages/components/src/theme/colorUtils.ts +++ b/packages/components/src/theme/colorUtils.ts @@ -246,6 +246,8 @@ export function isDHColorValue(value: string): value is DHColorValue { * ex. colorValueStyle('red') => 'red' * ex. colorValueStyle('#F00') => '#F00' */ +export function colorValueStyle(value: string): string; +export function colorValueStyle(value: string | undefined): string | undefined; export function colorValueStyle(value: string | undefined): string | undefined { if (value != null && isDHColorValue(value)) { return `var(--dh-color-${value})`; diff --git a/packages/grid/src/DataBarCellRenderer.ts b/packages/grid/src/DataBarCellRenderer.ts index bd8f0a3a0b..9b0db224b5 100644 --- a/packages/grid/src/DataBarCellRenderer.ts +++ b/packages/grid/src/DataBarCellRenderer.ts @@ -4,7 +4,7 @@ import CellRenderer from './CellRenderer'; import { isExpandableGridModel } from './ExpandableGridModel'; import { isDataBarGridModel } from './DataBarGridModel'; import { ModelIndex, VisibleIndex, VisibleToModelMap } from './GridMetrics'; -import GridColorUtils, { Oklab } from './GridColorUtils'; +import GridColorUtils from './GridColorUtils'; import GridUtils from './GridUtils'; import memoizeClear from './memoizeClear'; import { DEFAULT_FONT_WIDTH, GridRenderState } from './GridRendererTypes'; @@ -31,7 +31,41 @@ interface DataBarRenderMetrics { markerXs: number[]; } class DataBarCellRenderer extends CellRenderer { - private heightOfDigits?: number; + static getGradient = memoizeClear( + (width: number, colors: string[]): CanvasGradient => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (ctx == null) { + throw new Error('Failed to create canvas context'); + } + if (Number.isNaN(width)) { + return ctx.createLinearGradient(0, 0, 0, 0); + } + const gradient = ctx.createLinearGradient(0, 0, width, 0); + const oklabColors = colors.map(color => + GridColorUtils.linearSRGBToOklab(GridColorUtils.hexToRgb(color)) + ); + for (let i = 0; i < width; i += 1) { + const colorStop = i / width; + const colorChangeInterval = 1 / (colors.length - 1); + const leftColorIndex = Math.floor(colorStop / colorChangeInterval); + const color = GridColorUtils.lerpColor( + oklabColors[leftColorIndex], + oklabColors[leftColorIndex + 1], + (colorStop % colorChangeInterval) / colorChangeInterval + ); + gradient.addColorStop( + i / width, + GridColorUtils.rgbToHex(GridColorUtils.OklabToLinearSRGB(color)) + ); + } + return gradient; + }, + { + max: 1000, + primitive: true, // Stringify the arguments for memoization. Lets the color arrays be different arrays in memory, but still cache hit + } + ); drawCellContent( context: CanvasRenderingContext2D, @@ -59,11 +93,11 @@ class DataBarCellRenderer extends CellRenderer { const rowY = getOrThrow(allRowYs, row); const textAlign = model.textAlignForCell(modelColumn, modelRow); const text = model.textForCell(modelColumn, modelRow); - const { x: textX, width: textWidth } = GridUtils.getTextRenderMetrics( - state, - column, - row - ); + const { + x: textX, + y: textY, + width: textWidth, + } = GridUtils.getTextRenderMetrics(state, column, row); const fontWidth = fontWidths?.get(context.font) ?? DEFAULT_FONT_WIDTH; const truncationChar = model.truncationCharForCell(modelColumn, modelRow); @@ -87,7 +121,7 @@ class DataBarCellRenderer extends CellRenderer { value, } = model.dataBarOptionsForCell(modelColumn, modelRow, theme); - const hasGradient = Array.isArray(dataBarColor); + const hasGradient = Array.isArray(dataBarColor) && dataBarColor.length > 1; if (columnMin == null || columnMax == null) { return; } @@ -103,12 +137,6 @@ class DataBarCellRenderer extends CellRenderer { dataBarWidth, } = this.getDataBarRenderMetrics(context, state, column, row); - if (this.heightOfDigits === undefined) { - const { actualBoundingBoxAscent, actualBoundingBoxDescent } = - context.measureText('1234567890'); - this.heightOfDigits = actualBoundingBoxAscent + actualBoundingBoxDescent; - } - context.save(); context.textAlign = textAlign; if (hasGradient) { @@ -116,153 +144,101 @@ class DataBarCellRenderer extends CellRenderer { value >= 0 ? dataBarColor[dataBarColor.length - 1] : dataBarColor[0]; context.fillStyle = color; } else { - context.fillStyle = dataBarColor; + context.fillStyle = Array.isArray(dataBarColor) + ? dataBarColor[0] + : dataBarColor; } - context.textBaseline = 'top'; + context.textBaseline = 'middle'; context.font = theme.font; if (valuePlacement !== 'hide') { - context.fillText( - truncatedText, - textX, - rowY + (rowHeight - this.heightOfDigits) / 2 - ); + context.fillText(truncatedText, textX, textY); } + const hasRowDividers = theme.gridRowColor != null; + const yOffset = hasRowDividers ? 2 : 1; + + context.save(); + context.beginPath(); + context.roundRect( + dataBarX, + rowY + yOffset, // yOffset includes 1px for top padding + dataBarWidth, + rowHeight - 1 - yOffset, // 1px for bottom padding + 1 + ); + context.clip(); + context.globalAlpha = opacity; + // Draw bar if (hasGradient) { // Draw gradient bar - const dataBarColorsOklab: Oklab[] = dataBarColor.map(color => - GridColorUtils.linearSRGBToOklab(GridColorUtils.hexToRgb(color)) - ); - + let gradientWidth = 0; + let gradientX = 0; context.save(); - context.beginPath(); - - context.roundRect(dataBarX, dataBarY, dataBarWidth, rowHeight - 2, 1); - context.clip(); - + // Translate the context so its origin is at the start of the gradient + // and increasing x value moves towards the end of the gradient. + // For RTL, scale x by -1 to flip across the x-axis if (value < 0) { if (direction === 'LTR') { - const totalGradientWidth = Math.round( + gradientWidth = Math.round( (Math.abs(columnMin) / totalValueRange) * maxWidth ); - const partGradientWidth = - totalGradientWidth / (dataBarColor.length - 1); - let gradientX = Math.round(leftmostPosition); - for (let i = 0; i < dataBarColor.length - 1; i += 1) { - const leftColor = dataBarColorsOklab[i]; - const rightColor = dataBarColorsOklab[i + 1]; - this.drawGradient( - context, - leftColor, - rightColor, - gradientX, - rowY + 1, - partGradientWidth, - rowHeight - ); - - gradientX += partGradientWidth; - } + gradientX = Math.round(leftmostPosition); + context.translate(gradientX, 0); } else if (direction === 'RTL') { - const totalGradientWidth = Math.round( + gradientWidth = Math.round( maxWidth - (Math.abs(columnMax) / totalValueRange) * maxWidth ); - const partGradientWidth = - totalGradientWidth / (dataBarColor.length - 1); - let gradientX = Math.round(zeroPosition); - for (let i = dataBarColor.length - 1; i > 0; i -= 1) { - const leftColor = dataBarColorsOklab[i]; - const rightColor = dataBarColorsOklab[i - 1]; - this.drawGradient( - context, - leftColor, - rightColor, - gradientX, - rowY + 1, - partGradientWidth, - rowHeight - ); - - gradientX += partGradientWidth; - } + gradientX = Math.round(zeroPosition); + context.translate(gradientX + gradientWidth, 0); + context.scale(-1, 1); } } else if (direction === 'LTR') { // Value is greater than or equal to 0 - const totalGradientWidth = + gradientWidth = Math.round( maxWidth - (Math.abs(columnMin) / totalValueRange) * maxWidth ) - 1; - const partGradientWidth = - totalGradientWidth / (dataBarColor.length - 1); - let gradientX = Math.round(zeroPosition); - - for (let i = 0; i < dataBarColor.length - 1; i += 1) { - const leftColor = dataBarColorsOklab[i]; - const rightColor = dataBarColorsOklab[i + 1]; - this.drawGradient( - context, - leftColor, - rightColor, - gradientX, - rowY + 1, - partGradientWidth, - rowHeight - 2 - ); - - gradientX += partGradientWidth; - } + gradientX = Math.round(zeroPosition); + context.translate(gradientX, 0); } else if (direction === 'RTL') { // Value is greater than or equal to 0 - const totalGradientWidth = Math.round( + gradientWidth = Math.round( (Math.abs(columnMax) / totalValueRange) * maxWidth ); - const partGradientWidth = - totalGradientWidth / (dataBarColor.length - 1); - let gradientX = Math.round(leftmostPosition); - - for (let i = dataBarColor.length - 1; i > 0; i -= 1) { - const leftColor = dataBarColorsOklab[i]; - const rightColor = dataBarColorsOklab[i - 1]; - this.drawGradient( - context, - leftColor, - rightColor, - gradientX, - rowY + 1, - partGradientWidth, - rowHeight - 2 - ); - - gradientX += partGradientWidth; - } + gradientX = Math.round(leftmostPosition); + context.translate(gradientX + gradientWidth, 0); + context.scale(-1, 1); } - // restore clip - context.restore(); + const gradient = DataBarCellRenderer.getGradient( + gradientWidth, + dataBarColor + ); + context.fillStyle = gradient; + context.fillRect(0, dataBarY, gradientWidth, rowHeight); + context.restore(); // Restore gradient translate/scale } else { // Draw normal bar - context.save(); - - context.globalAlpha = opacity; context.beginPath(); - context.roundRect(dataBarX, dataBarY, dataBarWidth, rowHeight - 2, 1); + context.roundRect(dataBarX, dataBarY, dataBarWidth, rowHeight, 1); context.fill(); - - context.restore(); } // Draw markers if (maxWidth > 0) { markerXs.forEach((markerX, index) => { context.fillStyle = markers[index].color; - context.fillRect(markerX, dataBarY, 1, rowHeight - 2); + context.fillRect(markerX, dataBarY, 1, rowHeight); }); } + // restore clip + context.restore(); + const shouldRenderDashedLine = !( axis === 'directional' && ((valuePlacement === 'beside' && @@ -523,7 +499,7 @@ class DataBarCellRenderer extends CellRenderer { return { maxWidth, x: dataBarX, - y: y + 1.5, + y, zeroPosition, leftmostPosition, rightmostPosition, @@ -533,46 +509,11 @@ class DataBarCellRenderer extends CellRenderer { }; } - drawGradient( - context: CanvasRenderingContext2D, - leftColor: Oklab, - rightColor: Oklab, - x: number, - y: number, - width: number, - height: number - ): void { - let currentColor = leftColor; - // Increase by 0.5 because half-pixel will render weird on different zooms - for (let currentX = x; currentX <= x + width; currentX += 0.5) { - this.drawGradientPart( - context, - currentX, - y, - 1, - height, - GridColorUtils.rgbToHex(GridColorUtils.OklabToLinearSRGB(currentColor)) - ); - - currentColor = GridColorUtils.lerpColor( - leftColor, - rightColor, - (currentX - x) / width - ); - } - } - - drawGradientPart( - context: CanvasRenderingContext2D, - x: number, - y: number, - width: number, - height: number, - color: string - ): void { - context.fillStyle = color; - context.fillRect(x, y, width, height); - } + getCachedWidth = memoizeClear( + (context: CanvasRenderingContext2D, text: string): number => + context.measureText(text).width, + { max: 10000 } + ); /** * Returns the width of the widest value in pixels @@ -590,7 +531,7 @@ class DataBarCellRenderer extends CellRenderer { const row = visibleRows[i]; const modelRow = getOrThrow(modelRows, row); const text = model.textForCell(column, modelRow); - widestValue = Math.max(widestValue, context.measureText(text).width); + widestValue = Math.max(widestValue, this.getCachedWidth(context, text)); } return widestValue; diff --git a/packages/iris-grid/src/CommonTypes.tsx b/packages/iris-grid/src/CommonTypes.tsx index 815b2336fe..a99ee66baa 100644 --- a/packages/iris-grid/src/CommonTypes.tsx +++ b/packages/iris-grid/src/CommonTypes.tsx @@ -59,7 +59,12 @@ export type InputFilter = { }; export interface UIRow { - data: Map; + /** + * The data in the row indexed by column number. + * If a column is not part of the columns array (i.e. it's hidden by the model/table), + * then it will be included by its name instead of index. + */ + data: Map; } export type UIViewportData = { diff --git a/packages/iris-grid/src/IrisGridTableModelTemplate.ts b/packages/iris-grid/src/IrisGridTableModelTemplate.ts index 0cd4166e74..e57eb299dd 100644 --- a/packages/iris-grid/src/IrisGridTableModelTemplate.ts +++ b/packages/iris-grid/src/IrisGridTableModelTemplate.ts @@ -799,7 +799,7 @@ class IrisGridTableModelTemplate< frontIndex += 1; }); - let backIndex = this.columnMap.size - 1; + let backIndex = this.columns.length - 1; backColumns?.forEach(name => { if (usedColumns.has(name)) { throw new Error( @@ -899,18 +899,6 @@ class IrisGridTableModelTemplate< ); } - getMemoizedColumnMap = memoize( - (tableColumns: DhType.Column[]): Map => { - const columnMap = new Map(); - tableColumns.forEach(col => columnMap.set(col.name, col)); - return columnMap; - } - ); - - get columnMap(): Map { - return this.getMemoizedColumnMap(this.table.columns); - } - get columnHeaderMaxDepth(): number { return this._columnHeaderMaxDepth ?? 1; } @@ -1169,13 +1157,12 @@ class IrisGridTableModelTemplate< } extractViewportRow(row: DhType.Row, columns: DhType.Column[]): R { - const data = new Map(); + const data = new Map(); for (let c = 0; c < columns.length; c += 1) { const column = columns[c]; const index = this.getColumnIndexByName(column.name); - assertNotNull(index); - data.set(index, { + data.set(index ?? column.name, { value: row.get(column), format: row.getFormat(column), }); @@ -2046,8 +2033,11 @@ class IrisGridTableModelTemplate< this.pendingNewDataMap.forEach(row => { const newRow: Record = {}; row.data.forEach(({ value }, columnIndex) => { - const column = this.columns[columnIndex]; - newRow[column.name] = value; + const columnName = + typeof columnIndex === 'string' + ? columnIndex + : this.columns[columnIndex].name; + newRow[columnName] = value; }); newRows.push(newRow); }); diff --git a/packages/iris-grid/src/IrisGridTreeTableModel.ts b/packages/iris-grid/src/IrisGridTreeTableModel.ts index 7018795c54..a49bece9bf 100644 --- a/packages/iris-grid/src/IrisGridTreeTableModel.ts +++ b/packages/iris-grid/src/IrisGridTreeTableModel.ts @@ -10,7 +10,7 @@ import type { dh as DhType } from '@deephaven/jsapi-types'; import Log from '@deephaven/log'; import { Formatter, TableUtils } from '@deephaven/jsapi-utils'; import { assertNotNull } from '@deephaven/utils'; -import { UIRow, ColumnName, CellData } from './CommonTypes'; +import { UIRow, ColumnName } from './CommonTypes'; import IrisGridTableModelTemplate from './IrisGridTableModelTemplate'; import { DisplayColumn } from './IrisGridModel'; @@ -129,7 +129,7 @@ class IrisGridTreeTableModel extends IrisGridTableModelTemplate< extractViewportRow(row: DhType.TreeRow, columns: DhType.Column[]): UITreeRow { const { isExpanded, hasChildren, depth } = row; const extractedRow = super.extractViewportRow(row, columns); - const modifiedData = new Map(extractedRow.data); + const modifiedData = new Map(extractedRow.data); if (hasChildren) { for (let i = 0; i < this.virtualColumns.length; i += 1) { const key = i + (depth - 1) + (this.virtualColumns.length - 1); diff --git a/packages/iris-grid/src/IrisGridUtils.ts b/packages/iris-grid/src/IrisGridUtils.ts index 461bbd73df..539ee7341b 100644 --- a/packages/iris-grid/src/IrisGridUtils.ts +++ b/packages/iris-grid/src/IrisGridUtils.ts @@ -1470,17 +1470,19 @@ class IrisGridUtils { pendingDataMap: ReadonlyMap< ModelIndex, { - data: Map; + data: Map; } > ): DehydratedPendingDataMap { return [...pendingDataMap].map(([rowIndex, { data }]) => [ rowIndex, { - data: [...data].map(([c, value]) => [ - columns[c].name, - this.dehydrateValue(value, columns[c].type), - ]), + data: [...data] + .filter(([c]) => typeof c === 'number') + .map(([c, value]) => [ + columns[c as number].name, + this.dehydrateValue(value, columns[c as number].type), + ]), }, ]); } diff --git a/tests/styleguide.spec.ts-snapshots/grids-data-bar-chromium-linux.png b/tests/styleguide.spec.ts-snapshots/grids-data-bar-chromium-linux.png index b8458fe986..572098ba7b 100644 Binary files a/tests/styleguide.spec.ts-snapshots/grids-data-bar-chromium-linux.png and b/tests/styleguide.spec.ts-snapshots/grids-data-bar-chromium-linux.png differ diff --git a/tests/styleguide.spec.ts-snapshots/grids-data-bar-firefox-linux.png b/tests/styleguide.spec.ts-snapshots/grids-data-bar-firefox-linux.png index 8ba6320655..bf772c6009 100644 Binary files a/tests/styleguide.spec.ts-snapshots/grids-data-bar-firefox-linux.png and b/tests/styleguide.spec.ts-snapshots/grids-data-bar-firefox-linux.png differ diff --git a/tests/styleguide.spec.ts-snapshots/grids-data-bar-webkit-linux.png b/tests/styleguide.spec.ts-snapshots/grids-data-bar-webkit-linux.png index fede745767..e7158c3a9a 100644 Binary files a/tests/styleguide.spec.ts-snapshots/grids-data-bar-webkit-linux.png and b/tests/styleguide.spec.ts-snapshots/grids-data-bar-webkit-linux.png differ