From a80c6fc608466351d03358f47b9c7d062b28c9cf Mon Sep 17 00:00:00 2001 From: Mike Bender Date: Mon, 15 May 2023 13:49:49 -0400 Subject: [PATCH] revert: Revert "feat: Table rendering support for databars (#1212)" (#1296) This reverts commit a17cc0eb2b4e8ba9240c891a15b9d4b7659fb721. - Noticed an issue when changing filters on a table with a string column which would cause `null` to be flashed in. Caused by the databar changes. We don't need the databars for this release so just reverting that change for now. ![Table shows null to start](https://github.com/deephaven/web-client-ui/assets/4505624/ced45b58-9caf-4f9e-8ffc-3218294db300) --- packages/code-studio/src/styleguide/Grids.tsx | 5 - .../grid-examples/DataBarExample.tsx | 123 ---- packages/grid/src/CellRenderer.ts | 104 --- packages/grid/src/DataBarCellRenderer.ts | 610 ------------------ packages/grid/src/DataBarGridModel.ts | 53 -- packages/grid/src/GridColorUtils.ts | 119 +--- packages/grid/src/GridMetricCalculator.ts | 15 +- packages/grid/src/GridModel.ts | 5 - packages/grid/src/GridRenderer.test.tsx | 42 +- packages/grid/src/GridRenderer.ts | 426 +++++++++++- packages/grid/src/GridRendererTypes.ts | 3 - packages/grid/src/GridTheme.ts | 13 - packages/grid/src/GridUtils.ts | 73 +-- packages/grid/src/MockDataBarGridModel.ts | 160 ----- packages/grid/src/TextCellRenderer.ts | 271 -------- packages/grid/src/TokenBoxCellRenderer.ts | 23 - packages/grid/src/index.ts | 5 - .../mouse-handlers/GridTokenMouseHandler.ts | 18 +- .../src/IrisGridCellRendererUtils.ts | 31 - .../src/IrisGridDataBarCellRenderer.ts | 32 - packages/iris-grid/src/IrisGridIcons.ts | 52 -- packages/iris-grid/src/IrisGridRenderer.ts | 339 +++++++--- .../src/IrisGridTableModelTemplate.ts | 11 - .../iris-grid/src/IrisGridTextCellRenderer.ts | 191 ------ .../iris-grid/src/IrisGridTheme.module.scss | 4 - packages/iris-grid/src/IrisGridTheme.ts | 4 - .../IrisGridTokenMouseHandler.ts | 17 +- 27 files changed, 668 insertions(+), 2081 deletions(-) delete mode 100644 packages/code-studio/src/styleguide/grid-examples/DataBarExample.tsx delete mode 100644 packages/grid/src/CellRenderer.ts delete mode 100644 packages/grid/src/DataBarCellRenderer.ts delete mode 100644 packages/grid/src/DataBarGridModel.ts delete mode 100644 packages/grid/src/MockDataBarGridModel.ts delete mode 100644 packages/grid/src/TextCellRenderer.ts delete mode 100644 packages/grid/src/TokenBoxCellRenderer.ts delete mode 100644 packages/iris-grid/src/IrisGridCellRendererUtils.ts delete mode 100644 packages/iris-grid/src/IrisGridDataBarCellRenderer.ts delete mode 100644 packages/iris-grid/src/IrisGridIcons.ts delete mode 100644 packages/iris-grid/src/IrisGridTextCellRenderer.ts diff --git a/packages/code-studio/src/styleguide/Grids.tsx b/packages/code-studio/src/styleguide/Grids.tsx index 27d07bf7bf..6c857bddc5 100644 --- a/packages/code-studio/src/styleguide/Grids.tsx +++ b/packages/code-studio/src/styleguide/Grids.tsx @@ -13,7 +13,6 @@ import StaticExample from './grid-examples/StaticExample'; import QuadrillionExample from './grid-examples/QuadrillionExample'; import TreeExample from './grid-examples/TreeExample'; import AsyncExample from './grid-examples/AsyncExample'; -import DataBarExample from './grid-examples/DataBarExample'; function Grids(): ReactElement { const [irisGridModel] = useState( @@ -35,10 +34,6 @@ function Grids(): ReactElement {
-

Data Bar

-
- -

Quadrillion rows and columns

diff --git a/packages/code-studio/src/styleguide/grid-examples/DataBarExample.tsx b/packages/code-studio/src/styleguide/grid-examples/DataBarExample.tsx deleted file mode 100644 index 9718b2399d..0000000000 --- a/packages/code-studio/src/styleguide/grid-examples/DataBarExample.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React, { useState } from 'react'; -import { Grid, MockDataBarGridModel } from '@deephaven/grid'; -import { ColorMap } from 'packages/grid/src/DataBarGridModel'; - -function DataBarExample() { - const columnData = [100, 50, 20, 10, -10, -20, -50, -30, 100, 0, 1]; - const data: number[][] = []; - const columnAxes = new Map([ - [0, 'proportional'], - [1, 'middle'], - [2, 'directional'], - [6, 'directional'], - [7, 'directional'], - [8, 'directional'], - [9, 'directional'], - [10, 'directional'], - ]); - const positiveColors: ColorMap = new Map([ - [3, '#72d7df'], - [4, '#ac9cf4'], - ]); - positiveColors.set(5, ['#f3cd5b', '#9edc6f']); - positiveColors.set(19, ['#42f54b', '#42b9f5', '#352aa8']); - - const negativeColors: ColorMap = new Map([ - [3, '#f3cd5b'], - [4, '#ac9cf4'], - ]); - negativeColors.set(5, ['#f95d84', '#f3cd5b']); - negativeColors.set(19, ['#e05536', '#e607de', '#e6e207']); - - const valuePlacements = new Map([ - [6, 'hide'], - [7, 'overlap'], - [8, 'overlap'], - [9, 'overlap'], - ]); - const opacities = new Map([ - [7, 0.5], - [8, 0.5], - [9, 0.5], - ]); - const directions = new Map([ - [8, 'RTL'], - [10, 'RTL'], - [16, 'RTL'], - [19, 'RTL'], - ]); - const textAlignments = new Map([ - [9, 'left'], - [11, 'left'], - ]); - const markers = new Map([ - [ - 12, - [ - { column: 13, color: 'white' }, - { column: 14, color: 'gray' }, - ], - ], - ]); - for (let i = 0; i < 13; i += 1) { - data.push(columnData.slice()); - } - data.push([70, 60, 30, 20, -10, -30, -20, -50, 80, 50, 10]); - data.push([50, 20, 10, 0, 0, -10, -30, 10, 90, 20, 40]); - data.push([-100, -90, -80, -70, -60, -50, -40, -30, -20, -10, 0]); - data.push(columnData.slice()); - // Decimals - data.push([ - 100, - 10.5, - 11.234, - -20.5, - -50, - -2.5, - -15.1234, - 94.254, - 25, - 44.4444, - -50.5, - ]); - - // Big values - data.push([ - 1000000, - 10, - 200, - -20000, - -2000000, - -25, - -900000, - 800000, - 100000, - 450000, - 1, - ]); - - // RTL gradient with multiple colors - data.push(columnData.slice()); - - // Both data bar and text - data.push(columnData.slice()); - data.push(columnData.slice()); - const [model] = useState( - () => - new MockDataBarGridModel( - data, - columnAxes, - positiveColors, - negativeColors, - valuePlacements, - opacities, - directions, - textAlignments, - markers - ) - ); - - return ; -} - -export default DataBarExample; diff --git a/packages/grid/src/CellRenderer.ts b/packages/grid/src/CellRenderer.ts deleted file mode 100644 index 0326a9442d..0000000000 --- a/packages/grid/src/CellRenderer.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* eslint-disable class-methods-use-this */ -import { getOrThrow } from '@deephaven/utils'; -import { isExpandableGridModel } from './ExpandableGridModel'; -import { VisibleIndex, Coordinate, BoxCoordinates } from './GridMetrics'; -import GridRenderer from './GridRenderer'; -import { GridRenderState } from './GridRendererTypes'; -import { GridColor } from './GridTheme'; -import memoizeClear from './memoizeClear'; - -export type CellRenderType = 'text' | 'dataBar'; - -abstract class CellRenderer { - abstract drawCellContent( - context: CanvasRenderingContext2D, - state: GridRenderState, - column: VisibleIndex, - row: VisibleIndex - ): void; - - drawCellRowTreeMarker( - context: CanvasRenderingContext2D, - state: GridRenderState, - row: VisibleIndex - ): void { - const { metrics, model, mouseX, mouseY, theme } = state; - const { - firstColumn, - gridX, - gridY, - allColumnXs, - allColumnWidths, - allRowYs, - allRowHeights, - visibleRowTreeBoxes, - } = metrics; - const { treeMarkerColor, treeMarkerHoverColor } = theme; - const columnX = getOrThrow(allColumnXs, firstColumn); - const columnWidth = getOrThrow(allColumnWidths, firstColumn); - const rowY = getOrThrow(allRowYs, row); - const rowHeight = getOrThrow(allRowHeights, row); - if (!isExpandableGridModel(model) || !model.isRowExpandable(row)) { - return; - } - - const treeBox = getOrThrow(visibleRowTreeBoxes, row); - const color = - mouseX != null && - mouseY != null && - mouseX >= gridX + columnX && - mouseX <= gridX + columnX + columnWidth && - mouseY >= gridY + rowY && - mouseY <= gridY + rowY + rowHeight - ? treeMarkerHoverColor - : treeMarkerColor; - - this.drawTreeMarker( - context, - state, - columnX, - rowY, - treeBox, - color, - model.isRowExpanded(row) - ); - } - - drawTreeMarker( - context: CanvasRenderingContext2D, - state: GridRenderState, - columnX: Coordinate, - rowY: Coordinate, - treeBox: BoxCoordinates, - color: GridColor, - isExpanded: boolean - ): void { - const { x1, y1, x2, y2 } = treeBox; - const markerText = isExpanded ? '⊟' : '⊞'; - const textX = columnX + (x1 + x2) * 0.5 + 0.5; - const textY = rowY + (y1 + y2) * 0.5 + 0.5; - context.fillStyle = color; - context.textAlign = 'center'; - context.fillText(markerText, textX, textY); - } - - getCachedTruncatedString = memoizeClear( - ( - context: CanvasRenderingContext2D, - text: string, - width: number, - fontWidth: number, - truncationChar?: string - ): string => - GridRenderer.truncateToWidth( - context, - text, - width, - fontWidth, - truncationChar - ), - { max: 10000 } - ); -} - -export default CellRenderer; diff --git a/packages/grid/src/DataBarCellRenderer.ts b/packages/grid/src/DataBarCellRenderer.ts deleted file mode 100644 index 0b47b0258a..0000000000 --- a/packages/grid/src/DataBarCellRenderer.ts +++ /dev/null @@ -1,610 +0,0 @@ -/* eslint-disable class-methods-use-this */ -import { getOrThrow } from '@deephaven/utils'; -import CellRenderer from './CellRenderer'; -import { isExpandableGridModel } from './ExpandableGridModel'; -import { isDataBarGridModel } from './DataBarGridModel'; -import { ModelIndex, VisibleIndex, VisibleToModelMap } from './GridMetrics'; -import GridColorUtils, { Oklab } from './GridColorUtils'; -import GridUtils from './GridUtils'; -import memoizeClear from './memoizeClear'; -import { DEFAULT_FONT_WIDTH, GridRenderState } from './GridRendererTypes'; -import GridModel from './GridModel'; - -interface DataBarRenderMetrics { - /** The total width the entire bar from the min to max value can take up (rightmostPosition - leftmostPosition) */ - maxWidth: number; - /** The x coordinate of the bar (the left) */ - x: number; - /** The y coordinate of the bar (the top) */ - y: number; - /** The position of the zero line */ - zeroPosition: number; - /** The position of the leftmost point */ - leftmostPosition: number; - /** The position of the rightmost point */ - rightmostPosition: number; - /** The range of values (e.g. max of 100 and min of -50 means range of 150) */ - totalValueRange: number; - /** The width of the databar */ - dataBarWidth: number; - /** The x coordinates of the markers (the left) */ - markerXs: number[]; -} -class DataBarCellRenderer extends CellRenderer { - private heightOfDigits?: number; - - drawCellContent( - context: CanvasRenderingContext2D, - state: GridRenderState, - column: VisibleIndex, - row: VisibleIndex - ) { - const { metrics, model, theme } = state; - if (!isDataBarGridModel(model)) { - return; - } - const { - modelColumns, - modelRows, - allRowHeights, - allRowYs, - firstColumn, - fontWidths, - } = metrics; - - const isFirstColumn = column === firstColumn; - const rowHeight = getOrThrow(allRowHeights, row); - const modelRow = getOrThrow(modelRows, row); - const modelColumn = getOrThrow(modelColumns, column); - 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 fontWidth = fontWidths?.get(context.font) ?? DEFAULT_FONT_WIDTH; - const truncationChar = model.truncationCharForCell(modelColumn, modelRow); - const truncatedText = this.getCachedTruncatedString( - context, - text, - textWidth, - fontWidth, - truncationChar - ); - - const { - columnMin, - columnMax, - axis, - color: dataBarColor, - valuePlacement, - opacity, - markers, - direction, - value, - } = model.dataBarOptionsForCell(modelColumn, modelRow); - - const hasGradient = Array.isArray(dataBarColor); - if (columnMin == null || columnMax == null) { - return; - } - - const { - maxWidth, - x: dataBarX, - y: dataBarY, - zeroPosition, - leftmostPosition, - markerXs, - totalValueRange, - 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) { - const color = - value >= 0 ? dataBarColor[dataBarColor.length - 1] : dataBarColor[0]; - context.fillStyle = color; - } else { - context.fillStyle = dataBarColor; - } - context.textBaseline = 'top'; - context.font = theme.font; - - if (valuePlacement !== 'hide') { - context.fillText( - truncatedText, - textX, - rowY + (rowHeight - this.heightOfDigits) / 2 - ); - } - - // Draw bar - if (hasGradient) { - // Draw gradient bar - - const dataBarColorsOklab: Oklab[] = dataBarColor.map(color => - GridColorUtils.linearSRGBToOklab(GridColorUtils.hexToRgb(color)) - ); - - context.save(); - - context.beginPath(); - - context.roundRect(dataBarX, dataBarY, dataBarWidth, rowHeight - 2, 1); - context.clip(); - - if (value < 0) { - if (direction === 'LTR') { - const totalGradientWidth = 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; - } - } else if (direction === 'RTL') { - const totalGradientWidth = 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; - } - } - } else if (direction === 'LTR') { - // Value is greater than or equal to 0 - const totalGradientWidth = - 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; - } - } else if (direction === 'RTL') { - // Value is greater than or equal to 0 - const totalGradientWidth = 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; - } - } - - // restore clip - context.restore(); - } else { - // Draw normal bar - context.save(); - - context.globalAlpha = opacity; - context.beginPath(); - context.roundRect(dataBarX, dataBarY, dataBarWidth, rowHeight - 2, 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); - }); - } - - const shouldRenderDashedLine = !( - axis === 'directional' && - ((valuePlacement === 'beside' && - textAlign === 'right' && - direction === 'LTR') || - (valuePlacement === 'beside' && - textAlign === 'left' && - direction === 'RTL') || - valuePlacement !== 'beside') - ); - - // Draw dashed line - if (shouldRenderDashedLine) { - context.strokeStyle = theme.zeroLineColor; - context.beginPath(); - context.setLineDash([2, 1]); - context.moveTo(zeroPosition, rowY); - context.lineTo(zeroPosition, rowY + rowHeight); - context.stroke(); - } - - context.restore(); - - // Draw tree marker - if ( - isFirstColumn && - isExpandableGridModel(model) && - model.hasExpandableRows - ) { - this.drawCellRowTreeMarker(context, state, row); - } - } - - getDataBarRenderMetrics( - context: CanvasRenderingContext2D, - state: GridRenderState, - column: VisibleIndex, - row: VisibleIndex - ): DataBarRenderMetrics { - const { metrics, model, theme } = state; - if (!isDataBarGridModel(model)) { - throw new Error('Grid model is not a data bar grid model'); - } - const { - firstColumn, - allColumnXs, - allColumnWidths, - allRowYs, - modelColumns, - modelRows, - visibleRows, - } = metrics; - const { - cellHorizontalPadding, - treeDepthIndent, - treeHorizontalPadding, - } = theme; - - const modelColumn = getOrThrow(modelColumns, column); - const modelRow = getOrThrow(modelRows, row); - const x = getOrThrow(allColumnXs, column); - const y = getOrThrow(allRowYs, row); - const columnWidth = getOrThrow(allColumnWidths, column); - const isFirstColumn = column === firstColumn; - let treeIndent = 0; - if ( - isExpandableGridModel(model) && - model.hasExpandableRows && - isFirstColumn - ) { - treeIndent = - treeDepthIndent * (model.depthForRow(row) + 1) + treeHorizontalPadding; - } - - const textAlign = model.textAlignForCell(modelColumn, modelRow); - const { - columnMin, - columnMax, - axis, - valuePlacement, - markers, - direction, - value, - } = model.dataBarOptionsForCell(modelColumn, modelRow); - const longestValueWidth = this.getCachedWidestValueForColumn( - context, - visibleRows, - modelRows, - model, - modelColumn - ); - - const leftPadding = 2; - const rightPadding = - valuePlacement === 'beside' && textAlign === 'right' ? 2 : 1; - - // The value of the total range (e.g. max - column) - let totalValueRange = columnMax - columnMin; - // If min and max are both positive or min and max are equal, the max length is columnMax - if ((columnMax >= 0 && columnMin >= 0) || columnMin === columnMax) { - totalValueRange = columnMax; - } else if (columnMax <= 0 && columnMin <= 0) { - // If min and max are both negative, the max length is the absolute value of columnMin - totalValueRange = Math.abs(columnMin); - } - - let maxWidth = columnWidth - treeIndent - rightPadding - leftPadding; - if (valuePlacement === 'beside') { - maxWidth = maxWidth - cellHorizontalPadding - longestValueWidth; - } - - if (maxWidth < 0) { - maxWidth = 0; - } - - const columnLongest = Math.max(Math.abs(columnMin), Math.abs(columnMax)); - // If axis is proportional, totalValueRange is proportional to maxWidth - let dataBarWidth = (Math.abs(value) / totalValueRange) * maxWidth; - - if (maxWidth === 0) { - dataBarWidth = 0; - } else if (axis === 'middle') { - // The longest bar is proportional to half of the maxWidth - dataBarWidth = (Math.abs(value) / columnLongest) * (maxWidth / 2); - } else if (axis === 'directional') { - // The longest bar is proportional to the maxWidth - dataBarWidth = (Math.abs(value) / columnLongest) * maxWidth; - } - - // Default: proportional, beside, LTR, right text align - // All positions are assuming the left side is 0 and the right side is maxWidth - let zeroPosition = - columnMin >= 0 ? 0 : (Math.abs(columnMin) / totalValueRange) * maxWidth; - let dataBarX = - value >= 0 - ? zeroPosition - : zeroPosition - (Math.abs(value) / totalValueRange) * maxWidth; - let markerXs = markers.map(marker => { - const { column: markerColumn } = marker; - const markerValue = Number(model.textForCell(markerColumn, modelRow)); - return markerValue >= 0 - ? zeroPosition + (Math.abs(markerValue) / totalValueRange) * maxWidth - : zeroPosition - (Math.abs(markerValue) / totalValueRange) * maxWidth; - }); - let leftmostPosition = - valuePlacement === 'beside' && textAlign === 'left' - ? cellHorizontalPadding + longestValueWidth + leftPadding - : leftPadding; - let rightmostPosition = - valuePlacement === 'beside' && textAlign === 'right' - ? columnWidth - cellHorizontalPadding - longestValueWidth - rightPadding - : rightPadding; - - // Proportional, RTL - if (direction === 'RTL') { - zeroPosition = - columnMin >= 0 - ? columnWidth - : columnWidth - (Math.abs(columnMin) / totalValueRange) * maxWidth; - dataBarX = - value >= 0 - ? zeroPosition - (value / totalValueRange) * maxWidth - : zeroPosition; - markerXs = markers.map(marker => { - const { column: markerColumn } = marker; - const markerValue = Number(model.textForCell(markerColumn, modelRow)); - return markerValue >= 0 - ? zeroPosition - (Math.abs(markerValue) / totalValueRange) * maxWidth - : zeroPosition + (Math.abs(markerValue) / totalValueRange) * maxWidth; - }); - } - - if (axis === 'middle') { - zeroPosition = maxWidth / 2; - if (direction === 'LTR') { - // Middle, LTR - dataBarX = - value >= 0 - ? zeroPosition - : zeroPosition - (Math.abs(value) / columnLongest) * (maxWidth / 2); - markerXs = markers.map(marker => { - const { column: markerColumn } = marker; - const markerValue = Number(model.textForCell(markerColumn, modelRow)); - return markerValue >= 0 - ? zeroPosition + - (Math.abs(markerValue) / columnLongest) * (maxWidth / 2) - : zeroPosition - - (Math.abs(markerValue) / columnLongest) * (maxWidth / 2); - }); - } else if (direction === 'RTL') { - // Middle, RTL - dataBarX = - value <= 0 - ? zeroPosition - : zeroPosition - (Math.abs(value) / columnLongest) * (maxWidth / 2); - markerXs = markers.map(marker => { - const { column: markerColumn } = marker; - const markerValue = Number(model.textForCell(markerColumn, modelRow)); - return markerValue <= 0 - ? zeroPosition + - (Math.abs(markerValue) / columnLongest) * (maxWidth / 2) - : zeroPosition - - (Math.abs(markerValue) / columnLongest) * (maxWidth / 2); - }); - } - } else if (axis === 'directional') { - if (direction === 'LTR') { - // Directional, LTR - zeroPosition = 0; - dataBarX = zeroPosition; - markerXs = markers.map(marker => { - const { column: markerColumn } = marker; - const markerValue = Number(model.textForCell(markerColumn, modelRow)); - return ( - zeroPosition + (Math.abs(markerValue) / columnLongest) * maxWidth - ); - }); - } else if (direction === 'RTL') { - // Directional, RTL - zeroPosition = columnWidth; - dataBarX = zeroPosition - (Math.abs(value) / columnLongest) * maxWidth; - markerXs = markers.map(marker => { - const { column: markerColumn } = marker; - const markerValue = Number(model.textForCell(markerColumn, modelRow)); - return ( - zeroPosition - (Math.abs(markerValue) / columnLongest) * maxWidth - ); - }); - } - } - - // Offset all values by the actual x value and padding - if (direction === 'LTR') { - zeroPosition += x + leftPadding + treeIndent; - dataBarX += x + leftPadding + treeIndent; - markerXs = markerXs.map( - markerX => markerX + x + leftPadding + treeIndent - ); - - if (valuePlacement === 'beside' && textAlign === 'left') { - zeroPosition += longestValueWidth + cellHorizontalPadding; - dataBarX += longestValueWidth + cellHorizontalPadding; - markerXs = markerXs.map( - markerX => markerX + longestValueWidth + cellHorizontalPadding - ); - } - } else if (direction === 'RTL') { - zeroPosition = zeroPosition + x - rightPadding; - dataBarX = dataBarX + x - rightPadding; - markerXs = markerXs.map(markerX => markerX + x - rightPadding); - - if (valuePlacement === 'beside' && textAlign === 'right') { - zeroPosition = zeroPosition - cellHorizontalPadding - longestValueWidth; - dataBarX = dataBarX - cellHorizontalPadding - longestValueWidth; - markerXs = markerXs.map( - markerX => markerX - cellHorizontalPadding - longestValueWidth - ); - } - } - - leftmostPosition += x + treeIndent; - rightmostPosition += x; - - return { - maxWidth, - x: dataBarX, - y: y + 1.5, - zeroPosition, - leftmostPosition, - rightmostPosition, - totalValueRange, - dataBarWidth, - markerXs, - }; - } - - drawGradient( - context: CanvasRenderingContext2D, - leftColor: Oklab, - rightColor: Oklab, - x: number, - y: number, - width: number, - height: number - ) { - 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 - ) { - context.fillStyle = color; - context.fillRect(x, y, width, height); - } - - /** - * Returns the width of the widest value in pixels - */ - getCachedWidestValueForColumn = memoizeClear( - ( - context: CanvasRenderingContext2D, - visibleRows: readonly VisibleIndex[], - modelRows: VisibleToModelMap, - model: GridModel, - column: ModelIndex - ): number => { - let widestValue = 0; - for (let i = 0; i < visibleRows.length; i += 1) { - const row = visibleRows[i]; - const modelRow = getOrThrow(modelRows, row); - const text = model.textForCell(column, modelRow); - widestValue = Math.max(widestValue, context.measureText(text).width); - } - - return widestValue; - }, - { max: 1000 } - ); -} - -export default DataBarCellRenderer; diff --git a/packages/grid/src/DataBarGridModel.ts b/packages/grid/src/DataBarGridModel.ts deleted file mode 100644 index e7a3dcab72..0000000000 --- a/packages/grid/src/DataBarGridModel.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { ModelIndex } from './GridMetrics'; -import GridModel from './GridModel'; -import { GridColor } from './GridTheme'; - -export type Marker = { column: ModelIndex; color: string }; -export type AxisOption = 'proportional' | 'middle' | 'directional'; -export type ValuePlacementOption = 'beside' | 'overlap' | 'hide'; -export type DirectionOption = 'LTR' | 'RTL'; -/** Map from ModelIndex to the axis option of the column */ -export type ColumnAxisMap = Map; -/** Map from ModelIndex to a color or an array of colors - * If given an array, then the bar will be a gradient - * The colors should be given left to right (e.g. it should be like ['yellow', 'green'] for positive color and ['red', 'yellow'] for negative color) - */ -export type ColorMap = Map; -/** Map from ModelIndex to the value placement option of the column */ -export type ValuePlacementMap = Map; -/** Map from ModelIndex to the opacity of the column */ -export type OpacityMap = Map; -/** Map from ModelIndex to the direction of the column */ -export type DirectionMap = Map; -/** Map from ModelIndex to the text alignment of the column */ -export type TextAlignmentMap = Map; -/** Map from column to the columns its markers are from */ -export type MarkerMap = Map; -/** Map from column to whether the bar has a gradient */ -export type GradientMap = Map; -// Map from ModelIndex to the minimum number in the column -export type MinMap = Map; -// Map from ModelIndex to the maximum number in the column -export type MaxMap = Map; - -export interface DataBarOptions { - columnMin: number; - columnMax: number; - axis: AxisOption; - color: GridColor | GridColor[]; - valuePlacement: ValuePlacementOption; - opacity: number; - markers: Marker[]; - direction: DirectionOption; - value: number; -} - -export function isDataBarGridModel( - model: GridModel -): model is DataBarGridModel { - return (model as DataBarGridModel)?.dataBarOptionsForCell !== undefined; -} - -export interface DataBarGridModel extends GridModel { - dataBarOptionsForCell(column: ModelIndex, row: ModelIndex): DataBarOptions; -} diff --git a/packages/grid/src/GridColorUtils.ts b/packages/grid/src/GridColorUtils.ts index 4a653611a7..6301ebc70d 100644 --- a/packages/grid/src/GridColorUtils.ts +++ b/packages/grid/src/GridColorUtils.ts @@ -1,11 +1,7 @@ import convert from 'color-convert'; import { HEX } from 'color-convert/conversions'; -import clamp from 'lodash.clamp'; import { GridColor } from './GridTheme'; -export type RGB = { r: number; g: number; b: number }; -export type Oklab = { L: number; a: number; b: number }; - /** * Darken the provided colour * @param color Color in hex format to convert (with #) @@ -34,117 +30,4 @@ export function colorWithAlpha(color: HEX, alpha: number): GridColor { return `rgba(${r}, ${g}, ${b}, ${alpha})`; } -/** - * Converts a color in RGB to Oklab - * Formula provided here: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab - * @param color An RGB color - * @returns The color but respresented as an Oklab color - */ -const linearSRGBToOklab = (color: RGB): Oklab => { - const { r, g, b } = color; - - const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b; - const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b; - const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b; - - const l2 = Math.cbrt(l); - const m2 = Math.cbrt(m); - const s2 = Math.cbrt(s); - - return { - L: 0.2104542553 * l2 + 0.793617785 * m2 - 0.0040720468 * s2, - a: 1.9779984951 * l2 - 2.428592205 * m2 + 0.4505937099 * s2, - b: 0.0259040371 * l2 + 0.7827717662 * m2 - 0.808675766 * s2, - }; -}; - -/** - * Converts an Oklab color to RGB - * Formula provided here: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab - * @param color An Oklab color - * @returns The given color but represented as a RGB color - */ -const OklabToLinearSRGB = (color: Oklab): RGB => { - const { L, a, b } = color; - - const l2 = L + 0.3963377774 * a + 0.2158037573 * b; - const m2 = L - 0.1055613458 * a - 0.0638541728 * b; - const s2 = L - 0.0894841775 * a - 1.291485548 * b; - - const l = l2 * l2 * l2; - const m = m2 * m2 * m2; - const s = s2 * s2 * s2; - - return { - r: clamp(+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, 0, 255), - g: clamp(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, 0, 255), - b: clamp(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s, 0, 255), - }; -}; - -/** - * Converts a hex color to RGB - * Algorithm from https://stackoverflow.com/a/39077686/20005358 - * @param hex A hex color - * @returns The RGB representation of the given color - */ -const hexToRgb = (hex: string): RGB => { - const rgbArray = hex - .replace( - /^#?([a-f\d])([a-f\d])([a-f\d])$/i, - (m: string, r: string, g: string, b: string) => - `#${r}${r}${g}${g}${b}${b}` - ) - .substring(1) - .match(/.{2}/g) - ?.map((x: string) => parseInt(x, 16)) ?? [0, 0, 0]; - - return { r: rgbArray[0], g: rgbArray[1], b: rgbArray[2] }; -}; - -/** - * Converts a RGB color to hex - * Algorithm from https://stackoverflow.com/a/39077686/20005358 - * @param color A RGB color - * @returns The hexcode of the given color - */ -const rgbToHex = (color: RGB): string => { - const r = Math.round(color.r); - const g = Math.round(color.g); - const b = Math.round(color.b); - - return `#${[r, g, b] - .map(x => { - const hex = x.toString(16); - return hex.length === 1 ? `0${hex}` : hex; - }) - .join('')}`; -}; - -/** - * Calculates a color given an interpolation factor between two given colors - * @param color1 Color on one end - * @param color2 Color on other end - * @param factor The interpolation factor (0 to 1, 0 will be color1 while 1 will be color2) - * @returns The color determined by the interpolation factor between the two colors - */ -const lerpColor = (color1: Oklab, color2: Oklab, factor: number): Oklab => { - const { L: L1, a: a1, b: b1 } = color1; - const { L: L2, a: a2, b: b2 } = color2; - - const L = L1 + (L2 - L1) * factor; - const a = a1 + (a2 - a1) * factor; - const b = b1 + (b2 - b1) * factor; - - return { L, a, b }; -}; - -export default { - colorWithAlpha, - darkenForDepth, - linearSRGBToOklab, - OklabToLinearSRGB, - hexToRgb, - rgbToHex, - lerpColor, -}; +export default { colorWithAlpha, darkenForDepth }; diff --git a/packages/grid/src/GridMetricCalculator.ts b/packages/grid/src/GridMetricCalculator.ts index bc49d116dd..782c7fb1e6 100644 --- a/packages/grid/src/GridMetricCalculator.ts +++ b/packages/grid/src/GridMetricCalculator.ts @@ -1728,7 +1728,6 @@ export class GridMetricCalculator { rowHeaderWidth, rowFooterWidth, scrollBarSize, - dataBarHorizontalPadding, } = theme; let columnWidth = 0; @@ -1745,19 +1744,13 @@ export class GridMetricCalculator { row => { const modelRow = this.getModelRow(row, state); const text = model.textForCell(modelColumn, modelRow); - const cellRenderType = model.renderTypeForCell(modelColumn, modelRow); - - let cellWidth = 0; if (text) { const cellPadding = cellHorizontalPadding * 2; - cellWidth = text.length * fontWidth + cellPadding; - } - - if (cellRenderType === 'dataBar') { - cellWidth += dataBarHorizontalPadding; + columnWidth = Math.max( + columnWidth, + text.length * fontWidth + cellPadding + ); } - - columnWidth = Math.max(columnWidth, cellWidth); } ); diff --git a/packages/grid/src/GridModel.ts b/packages/grid/src/GridModel.ts index 3f0dda5f7e..81bcacad5c 100644 --- a/packages/grid/src/GridModel.ts +++ b/packages/grid/src/GridModel.ts @@ -4,7 +4,6 @@ import { ModelIndex } from './GridMetrics'; import { GridColor, GridTheme, NullableGridColor } from './GridTheme'; import memoizeClear from './memoizeClear'; import GridUtils, { Token } from './GridUtils'; -import { CellRenderType } from './CellRenderer'; const LINK_TRUNCATION_LENGTH = 5000; @@ -227,10 +226,6 @@ abstract class GridModel< return GridUtils.findTokensWithProtocolInText(contentToCheckForLinks); } ); - - renderTypeForCell(column: ModelIndex, row: ModelIndex): CellRenderType { - return 'text'; - } } export default GridModel; diff --git a/packages/grid/src/GridRenderer.test.tsx b/packages/grid/src/GridRenderer.test.tsx index 5d4891ceba..e5ba6efc57 100644 --- a/packages/grid/src/GridRenderer.test.tsx +++ b/packages/grid/src/GridRenderer.test.tsx @@ -3,7 +3,6 @@ import GridModel from './GridModel'; import GridRenderer from './GridRenderer'; import MockGridModel from './MockGridModel'; import GridTheme from './GridTheme'; -import TextCellRenderer from './TextCellRenderer'; import { LinkToken } from './GridUtils'; import { GridRenderState } from './GridRendererTypes'; @@ -117,10 +116,7 @@ describe('getTokenBoxesForVisibleCell', () => { }), }); - const textCellRenderer = renderer.getCellRenderer( - 'text' - ) as TextCellRenderer; - textCellRenderer.getCachedTruncatedString = jest.fn( + renderer.getCachedTruncatedString = jest.fn( ( context: CanvasRenderingContext2D, text: string, @@ -132,14 +128,7 @@ describe('getTokenBoxesForVisibleCell', () => { }); it('should return tokens that are visible in the cell', () => { - const textCellRenderer = renderer.getCellRenderer( - 'text' - ) as TextCellRenderer; - const tokens = textCellRenderer.getTokenBoxesForVisibleCell( - 0, - 0, - renderState - ); + const tokens = renderer.getTokenBoxesForVisibleCell(0, 0, renderState); const expectedValue: LinkToken = { type: 'url', @@ -155,14 +144,7 @@ describe('getTokenBoxesForVisibleCell', () => { }); it('should return multiple tokens', () => { - const textCellRenderer = renderer.getCellRenderer( - 'text' - ) as TextCellRenderer; - const tokens = textCellRenderer.getTokenBoxesForVisibleCell( - 0, - 2, - renderState - ); + const tokens = renderer.getTokenBoxesForVisibleCell(0, 2, renderState); const expectedValue: LinkToken[] = [ { @@ -191,14 +173,7 @@ describe('getTokenBoxesForVisibleCell', () => { }); it('should return empty array if there are no tokens', () => { - const textCellRenderer = renderer.getCellRenderer( - 'text' - ) as TextCellRenderer; - const tokens = textCellRenderer.getTokenBoxesForVisibleCell( - 0, - 1, - renderState - ); + const tokens = renderer.getTokenBoxesForVisibleCell(0, 1, renderState); expect(tokens).toHaveLength(0); }); @@ -206,14 +181,7 @@ describe('getTokenBoxesForVisibleCell', () => { it('should return empty array if context or metrics is null', () => { // @ts-expect-error metrics and context usually can't be null renderState = makeMockGridRenderState({ metrics: null, context: null }); - const textCellRenderer = renderer.getCellRenderer( - 'text' - ) as TextCellRenderer; - const tokens = textCellRenderer.getTokenBoxesForVisibleCell( - 0, - 0, - renderState - ); + const tokens = renderer.getTokenBoxesForVisibleCell(0, 0, renderState); expect(tokens).toHaveLength(0); }); diff --git a/packages/grid/src/GridRenderer.ts b/packages/grid/src/GridRenderer.ts index 38b142ea9e..e8744dafc2 100644 --- a/packages/grid/src/GridRenderer.ts +++ b/packages/grid/src/GridRenderer.ts @@ -1,18 +1,15 @@ import clamp from 'lodash.clamp'; -import { ColorUtils, getOrThrow } from '@deephaven/utils'; +import { ColorUtils, EMPTY_ARRAY, getOrThrow } from '@deephaven/utils'; import memoizeClear from './memoizeClear'; -import GridUtils from './GridUtils'; +import GridUtils, { Token, TokenBox } from './GridUtils'; import GridColorUtils from './GridColorUtils'; import { isExpandableGridModel } from './ExpandableGridModel'; import { GridColor, GridColorWay, NullableGridColor } from './GridTheme'; -import { Coordinate, VisibleIndex } from './GridMetrics'; +import { BoxCoordinates, Coordinate, VisibleIndex } from './GridMetrics'; import { isEditableGridModel } from './EditableGridModel'; import GridColumnSeparatorMouseHandler from './mouse-handlers/GridColumnSeparatorMouseHandler'; import { BoundedAxisRange } from './GridAxisRange'; -import { DEFAULT_FONT_WIDTH, GridRenderState } from './GridRendererTypes'; -import CellRenderer, { CellRenderType } from './CellRenderer'; -import DataBarCellRenderer from './DataBarCellRenderer'; -import TextCellRenderer from './TextCellRenderer'; +import { GridRenderState } from './GridRendererTypes'; type NoneNullColumnRange = { startColumn: number; endColumn: number }; @@ -27,16 +24,15 @@ type NoneNullRowRange = { startRow: number; endRow: number }; * your own methods to customize drawing of the grid (eg. Draw icons or special features) */ export class GridRenderer { + // Default font width in pixels if it cannot be retrieved from the context + static DEFAULT_FONT_WIDTH = 10; + // Default radius in pixels for corners for some elements (like the active cell) static DEFAULT_EDGE_RADIUS = 2; // Default width in pixels for the border of the active cell static ACTIVE_CELL_BORDER_WIDTH = 2; - protected textCellRenderer = new TextCellRenderer(); - - protected dataBarCellRenderer = new DataBarCellRenderer(); - /** * Truncate a string to the specified length and add ellipses if necessary * @param str The string to truncate @@ -122,7 +118,7 @@ export class GridRenderer { context: CanvasRenderingContext2D, str: string, width: number, - fontWidth = DEFAULT_FONT_WIDTH, + fontWidth = GridRenderer.DEFAULT_FONT_WIDTH, truncationChar?: string ): string { if (width <= 0 || str.length <= 0) { @@ -997,28 +993,242 @@ export class GridRenderer { context.restore(); } + /** + * Gets textWidth and X-Y position for a specific cell + * The textWidth returned is the width that the text can occupy accounting for any other cell markings + * The width accounts for tree table indents and cell padding, so it is the width the text may consume + * + * @param state GridRenderState to get the text metrics for + * @param column Column of cell to get text metrics for + * @param row Row of cell to get text metrics for + * @returns Object with width, x, and y of the text + */ + getTextRenderMetrics( + state: GridRenderState, + column: VisibleIndex, + row: VisibleIndex + ): { + width: number; + x: number; + y: number; + } { + const { metrics, model, theme } = state; + const { + firstColumn, + allColumnXs, + allColumnWidths, + allRowYs, + allRowHeights, + modelRows, + modelColumns, + } = metrics; + const { + cellHorizontalPadding, + treeDepthIndent, + treeHorizontalPadding, + } = theme; + + const modelRow = getOrThrow(modelRows, row); + const modelColumn = getOrThrow(modelColumns, column); + const textAlign = model.textAlignForCell(modelColumn, modelRow); + const x = getOrThrow(allColumnXs, column); + const y = getOrThrow(allRowYs, row); + const columnWidth = getOrThrow(allColumnWidths, column); + const rowHeight = getOrThrow(allRowHeights, row); + const isFirstColumn = column === firstColumn; + let treeIndent = 0; + if ( + isExpandableGridModel(model) && + model.hasExpandableRows && + isFirstColumn + ) { + treeIndent = + treeDepthIndent * (model.depthForRow(row) + 1) + treeHorizontalPadding; + } + const textWidth = columnWidth - treeIndent; + let textX = x + cellHorizontalPadding; + const textY = y + rowHeight * 0.5; + if (textAlign === 'right') { + textX = x + textWidth - cellHorizontalPadding; + } else if (textAlign === 'center') { + textX = x + textWidth * 0.5; + } + textX += treeIndent; + + return { + width: textWidth - cellHorizontalPadding * 2, + x: textX, + y: textY, + }; + } + drawCellContent( context: CanvasRenderingContext2D, state: GridRenderState, column: VisibleIndex, - row: VisibleIndex + row: VisibleIndex, + textOverride?: string ): void { - const { metrics, model } = state; - const { modelColumns, modelRows } = metrics; + const { metrics, model, theme } = state; + const { + firstColumn, + fontWidths, + modelColumns, + modelRows, + allRowHeights, + } = metrics; + const { textColor } = theme; + const rowHeight = getOrThrow(allRowHeights, row); const modelRow = getOrThrow(modelRows, row); const modelColumn = getOrThrow(modelColumns, column); - const renderType = model.renderTypeForCell(modelColumn, modelRow); - const cellRenderer = this.getCellRenderer(renderType); - cellRenderer.drawCellContent(context, state, column, row); + const text = textOverride ?? model.textForCell(modelColumn, modelRow); + const truncationChar = model.truncationCharForCell(modelColumn, modelRow); + const isFirstColumn = column === firstColumn; + + if (text && rowHeight > 0) { + const textAlign = model.textAlignForCell(modelColumn, modelRow) || 'left'; + context.textAlign = textAlign; + + const color = + model.colorForCell(modelColumn, modelRow, theme) || textColor; + context.fillStyle = color; + + context.save(); + + const { + width: textWidth, + x: textX, + y: textY, + } = this.getTextRenderMetrics(state, column, row); + + const fontWidth = + fontWidths.get(context.font) ?? GridRenderer.DEFAULT_FONT_WIDTH; + const truncatedText = this.getCachedTruncatedString( + context, + text, + textWidth, + fontWidth, + truncationChar + ); + + const tokens = model.tokensForCell( + modelColumn, + modelRow, + truncatedText.length + ); + + if (truncatedText) { + let tokenIndex = 0; + let textStart = 0; + let left = textX; + const { actualBoundingBoxDescent } = context.measureText(truncatedText); + + while (textStart < truncatedText.length) { + const nextToken = tokens[tokenIndex]; + const token = textStart === nextToken?.start ? nextToken : null; + const textEnd = + token?.end ?? nextToken?.start ?? truncatedText.length; + const value = truncatedText.substring(textStart, textEnd); + const { width } = context.measureText(value); + const widthOfUnderline = value.endsWith('…') + ? context.measureText(value.substring(0, value.length - 1)).width + : width; + + // Set the styling based on the token, then draw the text + if (token != null) { + context.fillStyle = theme.hyperlinkColor; + context.fillText(value, left, textY); + context.fillRect( + left, + textY + actualBoundingBoxDescent, + widthOfUnderline, + 1 + ); + } else { + context.fillStyle = color; + context.fillText(value, left, textY); + } + + left += width; + textStart = textEnd; + if (token != null) tokenIndex += 1; + } + } + context.restore(); + } + + if ( + isFirstColumn && + isExpandableGridModel(model) && + model.hasExpandableRows + ) { + this.drawCellRowTreeMarker(context, state, row); + } } - getCellRenderer(renderType: CellRenderType): CellRenderer { - switch (renderType) { - case 'dataBar': - return this.dataBarCellRenderer; - default: - return this.textCellRenderer; + drawCellRowTreeMarker( + context: CanvasRenderingContext2D, + state: GridRenderState, + row: VisibleIndex + ): void { + const { metrics, model, mouseX, mouseY, theme } = state; + const { + firstColumn, + gridX, + gridY, + allColumnXs, + allColumnWidths, + allRowYs, + allRowHeights, + visibleRowTreeBoxes, + } = metrics; + const { treeMarkerColor, treeMarkerHoverColor } = theme; + const columnX = getOrThrow(allColumnXs, firstColumn); + const columnWidth = getOrThrow(allColumnWidths, firstColumn); + const rowY = getOrThrow(allRowYs, row); + const rowHeight = getOrThrow(allRowHeights, row); + if (!isExpandableGridModel(model) || !model.isRowExpandable(row)) { + return; } + + const treeBox = getOrThrow(visibleRowTreeBoxes, row); + const color = + mouseX != null && + mouseY != null && + mouseX >= gridX + columnX && + mouseX <= gridX + columnX + columnWidth && + mouseY >= gridY + rowY && + mouseY <= gridY + rowY + rowHeight + ? treeMarkerHoverColor + : treeMarkerColor; + + this.drawTreeMarker( + context, + state, + columnX, + rowY, + treeBox, + color, + model.isRowExpanded(row) + ); + } + + drawTreeMarker( + context: CanvasRenderingContext2D, + state: GridRenderState, + columnX: Coordinate, + rowY: Coordinate, + treeBox: BoxCoordinates, + color: GridColor, + isExpanded: boolean + ): void { + const { x1, y1, x2, y2 } = treeBox; + const markerText = isExpanded ? '⊟' : '⊞'; + const textX = columnX + (x1 + x2) * 0.5 + 0.5; + const textY = rowY + (y1 + y2) * 0.5 + 0.5; + context.fillStyle = color; + context.textAlign = 'center'; + context.fillText(markerText, textX, textY); } drawCellRowTreeDepthLines( @@ -1085,6 +1295,24 @@ export class GridRenderer { } } + getCachedTruncatedString = memoizeClear( + ( + context: CanvasRenderingContext2D, + text: string, + width: number, + fontWidth: number, + truncationChar?: string + ): string => + GridRenderer.truncateToWidth( + context, + text, + width, + fontWidth, + truncationChar + ), + { max: 10000 } + ); + getCachedBackgroundColors = memoizeClear( (backgroundColors: GridColorWay, maxDepth: number): GridColor[][] => backgroundColors.split(' ').map(color => { @@ -1537,7 +1765,8 @@ export class GridRenderer { white, } = theme; const { fontWidths, width } = metrics; - const fontWidth = fontWidths.get(context.font) ?? DEFAULT_FONT_WIDTH; + const fontWidth = + fontWidths.get(context.font) ?? GridRenderer.DEFAULT_FONT_WIDTH; const maxWidth = columnWidth - headerHorizontalPadding * 2; const maxLength = maxWidth / fontWidth; @@ -2751,6 +2980,153 @@ export class GridRenderer { context.translate(-barLeft, -barTop); } + + /** + * Gets the token boxes that are visible in the cell + * @param column The visible column + * @param row The visible row + * @param state The GridRenderState + * @returns An array of TokenBox of visible tokens or empty array with coordinates relative to gridX and gridY + */ + getTokenBoxesForVisibleCell( + column: VisibleIndex, + row: VisibleIndex, + state: GridRenderState + ): TokenBox[] { + const { metrics, context, model, theme } = state; + + if (context == null || metrics == null) { + return (EMPTY_ARRAY as unknown) as TokenBox[]; + } + + const { modelRows, modelColumns } = metrics; + const modelRow = getOrThrow(modelRows, row); + const modelColumn = getOrThrow(modelColumns, column); + + const text = model.textForCell(modelColumn, modelRow); + const { width: textWidth, x: textX, y: textY } = this.getTextRenderMetrics( + state, + column, + row + ); + + const { fontWidths } = metrics; + + // Set the font and baseline and change it back after + context.save(); + this.configureContext(context, state); + + const fontWidth = + fontWidths?.get(context.font) ?? GridRenderer.DEFAULT_FONT_WIDTH; + const truncationChar = model.truncationCharForCell(modelColumn, modelRow); + const truncatedText = this.getCachedTruncatedString( + context, + text, + textWidth, + fontWidth, + truncationChar + ); + + const { + actualBoundingBoxAscent, + actualBoundingBoxDescent, + } = context.measureText(truncatedText); + const textHeight = actualBoundingBoxAscent + actualBoundingBoxDescent; + + const tokens = model.tokensForCell( + modelColumn, + modelRow, + truncatedText.length + ); + + // Check if the truncated text contains a link + if (tokens.length === 0) { + context.restore(); + return (EMPTY_ARRAY as unknown) as TokenBox[]; + } + + const cachedTokenBoxes = this.getCachedTokenBoxesForVisibleCell( + truncatedText, + tokens, + theme.font, + 'middle', + textHeight, + context + ).map(tokenBox => ({ + x1: tokenBox.x1 + textX, + y1: tokenBox.y1 + (textY - actualBoundingBoxAscent), + x2: tokenBox.x2 + textX, + y2: tokenBox.y2 + (textY - actualBoundingBoxAscent), + token: tokenBox.token, + })); + + context.restore(); + + return cachedTokenBoxes; + } + + /** + * Returns an array of token boxes with the coordinates relative to the top left corner of the text + */ + getCachedTokenBoxesForVisibleCell = memoizeClear( + ( + truncatedText: string, + tokens: Token[], + // _font and _baseline are passed in so value is re-calculated when they change + // They should already be set on the `context`, so they are not used in this method + _font: string, + _baseline: CanvasTextBaseline, + textHeight: number, + context: CanvasRenderingContext2D + ): TokenBox[] => { + const top = 0; + const bottom = textHeight; + + const tokenBoxes: TokenBox[] = []; + + // The index where the last token ended + let lastTokenEnd = 0; + // The width of the text preceding the current token + let currentTextWidth = 0; + // Loop through array and push them to array + for (let i = 0; i < tokens.length; i += 1) { + const token = tokens[i]; + const { start, end } = token; + // The last token value is calculated based on the full text so the value needs to be truncated + const value = + end > truncatedText.length + ? truncatedText.substring(start) + : token.value; + + // Add the width of the text in between this token and the last token + currentTextWidth += context.measureText( + truncatedText.substring(lastTokenEnd, start) + ).width; + const tokenWidth = context.measureText(value).width; + + // Check if the x position is less than the grid x, then tokenWidth should be shifted by gridX - startX + + const left = currentTextWidth; + const right = left + tokenWidth; + + const newTokenBox: TokenBox = { + x1: left, + y1: top, + x2: right, + y2: bottom, + token, + }; + + tokenBoxes.push(newTokenBox); + + lastTokenEnd = end; + currentTextWidth += tokenWidth; + } + + return tokenBoxes; + }, + { max: 10000 } + ); } export default GridRenderer; diff --git a/packages/grid/src/GridRendererTypes.ts b/packages/grid/src/GridRendererTypes.ts index fecf47c366..8b2ddc2f8d 100644 --- a/packages/grid/src/GridRendererTypes.ts +++ b/packages/grid/src/GridRendererTypes.ts @@ -5,9 +5,6 @@ import { GridTheme } from './GridTheme'; import { DraggingColumn } from './mouse-handlers/GridColumnMoveMouseHandler'; import { GridSeparator } from './mouse-handlers/GridSeparatorMouseHandler'; -// Default font width in pixels if it cannot be retrieved from the context -export const DEFAULT_FONT_WIDTH = 10; - export type EditingCellTextSelectionRange = [start: number, end: number]; export type EditingCell = { diff --git a/packages/grid/src/GridTheme.ts b/packages/grid/src/GridTheme.ts index 741e2c11ab..cde1afae61 100644 --- a/packages/grid/src/GridTheme.ts +++ b/packages/grid/src/GridTheme.ts @@ -132,12 +132,6 @@ export type GridTheme = { // Divider colors between the floating parts and the grid floatingDividerOuterColor: GridColor; floatingDividerInnerColor: GridColor; - - zeroLineColor: GridColor; - positiveBarColor: GridColor; - negativeBarColor: GridColor; - - dataBarHorizontalPadding: number; }; /** @@ -227,13 +221,6 @@ const defaultTheme: GridTheme = Object.freeze({ // Divider colors between the floating parts and the grid floatingDividerOuterColor: '#000000', floatingDividerInnerColor: '#cccccc', - - // Databar - zeroLineColor: '#888888', - positiveBarColor: '#00ff00', - negativeBarColor: '#ff0000', - - dataBarHorizontalPadding: 90, }); export default defaultTheme; diff --git a/packages/grid/src/GridUtils.ts b/packages/grid/src/GridUtils.ts index 46eb59b3ae..5c6a216e0c 100644 --- a/packages/grid/src/GridUtils.ts +++ b/packages/grid/src/GridUtils.ts @@ -1,7 +1,7 @@ import React from 'react'; import clamp from 'lodash.clamp'; import { find as linkifyFind } from 'linkifyjs'; -import { EMPTY_ARRAY, getOrThrow } from '@deephaven/utils'; +import { EMPTY_ARRAY } from '@deephaven/utils'; import GridRange, { GridRangeIndex } from './GridRange'; import { BoxCoordinates, @@ -23,8 +23,6 @@ import { isBoundedAxisRange, Range, } from './GridAxisRange'; -import { isExpandableGridModel } from './ExpandableGridModel'; -import { GridRenderState } from './GridRendererTypes'; export type GridPoint = { x: Coordinate; @@ -1443,75 +1441,6 @@ export class GridUtils { }; } - /** - * Gets textWidth and X-Y position for a specific cell - * The textWidth returned is the width that the text can occupy accounting for any other cell markings - * The width accounts for tree table indents and cell padding, so it is the width the text may consume - * - * @param state GridRenderState to get the text metrics for - * @param column Column of cell to get text metrics for - * @param row Row of cell to get text metrics for - * @returns Object with width, x, and y of the text - */ - static getTextRenderMetrics( - state: GridRenderState, - column: VisibleIndex, - row: VisibleIndex - ): { - width: number; - x: number; - y: number; - } { - const { metrics, model, theme } = state; - const { - firstColumn, - allColumnXs, - allColumnWidths, - allRowYs, - allRowHeights, - modelRows, - modelColumns, - } = metrics; - const { - cellHorizontalPadding, - treeDepthIndent, - treeHorizontalPadding, - } = theme; - - const modelRow = getOrThrow(modelRows, row); - const modelColumn = getOrThrow(modelColumns, column); - const textAlign = model.textAlignForCell(modelColumn, modelRow); - const x = getOrThrow(allColumnXs, column); - const y = getOrThrow(allRowYs, row); - const columnWidth = getOrThrow(allColumnWidths, column); - const rowHeight = getOrThrow(allRowHeights, row); - const isFirstColumn = column === firstColumn; - let treeIndent = 0; - if ( - isExpandableGridModel(model) && - model.hasExpandableRows && - isFirstColumn - ) { - treeIndent = - treeDepthIndent * (model.depthForRow(row) + 1) + treeHorizontalPadding; - } - const textWidth = columnWidth - treeIndent; - let textX = x + cellHorizontalPadding; - const textY = y + rowHeight * 0.5; - if (textAlign === 'right') { - textX = x + textWidth - cellHorizontalPadding; - } else if (textAlign === 'center') { - textX = x + textWidth * 0.5; - } - textX += treeIndent; - - return { - width: textWidth - cellHorizontalPadding * 2, - x: textX, - y: textY, - }; - } - /** * Finds tokens in text (urls, emails) that start with https:// or http:// * @param text The text to search in diff --git a/packages/grid/src/MockDataBarGridModel.ts b/packages/grid/src/MockDataBarGridModel.ts deleted file mode 100644 index c7d246c33f..0000000000 --- a/packages/grid/src/MockDataBarGridModel.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* eslint-disable class-methods-use-this */ -import { getOrThrow } from '@deephaven/utils'; -import { CellRenderType } from './CellRenderer'; -import { - AxisOption, - ColorMap, - ColumnAxisMap, - DataBarGridModel, - DataBarOptions, - DirectionMap, - MarkerMap, - MaxMap, - MinMap, - OpacityMap, - TextAlignmentMap, - ValuePlacementMap, -} from './DataBarGridModel'; -import { ModelIndex } from './GridMetrics'; -import GridModel from './GridModel'; -import GridTheme from './GridTheme'; - -const DEFAULT_AXIS: AxisOption = 'proportional'; -const DEFAULT_POSITIVE_COLOR = GridTheme.positiveBarColor; -const DEFAULT_NEGATIVE_COLOR = GridTheme.negativeBarColor; -const DEFAULT_VALUE_PLACEMENT = 'beside'; -const DEFAULT_DIRECTION = 'LTR'; -const DEFAULT_TEXT_ALIGNMENT = 'right'; - -function isArrayOfNumbers(value: unknown): value is number[] { - return Array.isArray(value) && value.every(item => typeof item === 'number'); -} - -class MockDataBarGridModel extends GridModel implements DataBarGridModel { - private numberOfColumns; - - private numberOfRows; - - private data: unknown[][]; - - columnMins: MinMap; - - columnMaxs: MaxMap; - - columnAxes: ColumnAxisMap; - - valuePlacements: ValuePlacementMap; - - directions: DirectionMap; - - positiveColors: ColorMap; - - negativeColors: ColorMap; - - // Opacities should be between 0 and 1 - opacities: OpacityMap; - - textAlignments: TextAlignmentMap; - - markers: MarkerMap; - - constructor( - data: unknown[][], - columnAxes = new Map(), - positiveColors = new Map(), - negativeColors = new Map(), - valuePlacements = new Map(), - opacities = new Map(), - directions = new Map(), - textAlignments = new Map(), - markers: MarkerMap = new Map() - ) { - super(); - - this.positiveColors = positiveColors; - this.negativeColors = negativeColors; - this.data = data; - this.columnAxes = columnAxes; - this.valuePlacements = valuePlacements; - this.opacities = opacities; - this.directions = directions; - this.textAlignments = textAlignments; - this.markers = markers; - this.numberOfRows = Math.max(...data.map(row => row.length)); - this.numberOfColumns = data.length; - this.columnMins = new Map(); - this.columnMaxs = new Map(); - - for (let i = 0; i < data.length; i += 1) { - const column = data[i]; - if (isArrayOfNumbers(column)) { - this.columnMins.set(i, Math.min(...column)); - this.columnMaxs.set(i, Math.max(...column)); - } - } - } - - get rowCount() { - return this.numberOfRows; - } - - get columnCount() { - return this.numberOfColumns; - } - - textForCell(column: number, row: number): string { - return `${this.data[column]?.[row]}`; - } - - textForColumnHeader(column: number): string { - return `${column}`; - } - - textAlignForCell(column: number, row: number): CanvasTextAlign { - return this.textAlignments.get(column) ?? DEFAULT_TEXT_ALIGNMENT; - } - - renderTypeForCell(column: ModelIndex, row: ModelIndex): CellRenderType { - if (column < 20) { - return 'dataBar'; - } - return column % 2 === row % 2 ? 'dataBar' : 'text'; - } - - dataBarOptionsForCell(column: ModelIndex, row: ModelIndex): DataBarOptions { - const columnMin = getOrThrow(this.columnMins, column); - const columnMax = getOrThrow(this.columnMaxs, column); - const axis = this.columnAxes.get(column) ?? DEFAULT_AXIS; - const valuePlacement = - this.valuePlacements.get(column) ?? DEFAULT_VALUE_PLACEMENT; - let opacity = this.opacities.get(column); - if (opacity == null || opacity > 1) { - opacity = 1; - } else if (opacity < 0) { - opacity = 0; - } - const direction = this.directions.get(column) ?? DEFAULT_DIRECTION; - const positiveColor = - this.positiveColors.get(column) ?? DEFAULT_POSITIVE_COLOR; - const negativeColor = - this.negativeColors.get(column) ?? DEFAULT_NEGATIVE_COLOR; - - const value = Number(this.data[column]?.[row]); - const color = value >= 0 ? positiveColor : negativeColor; - const markers = this.markers.get(column) ?? []; - - return { - columnMin, - columnMax, - axis, - color, - valuePlacement, - opacity, - markers, - direction, - value, - }; - } -} - -export default MockDataBarGridModel; diff --git a/packages/grid/src/TextCellRenderer.ts b/packages/grid/src/TextCellRenderer.ts deleted file mode 100644 index 685d237dba..0000000000 --- a/packages/grid/src/TextCellRenderer.ts +++ /dev/null @@ -1,271 +0,0 @@ -/* eslint-disable class-methods-use-this */ -import { EMPTY_ARRAY, getOrThrow } from '@deephaven/utils'; -import CellRenderer from './CellRenderer'; -import { isExpandableGridModel } from './ExpandableGridModel'; -import { VisibleIndex } from './GridMetrics'; -import { DEFAULT_FONT_WIDTH, GridRenderState } from './GridRendererTypes'; -import GridUtils, { TokenBox, Token } from './GridUtils'; -import memoizeClear from './memoizeClear'; -import TokenBoxCellRenderer from './TokenBoxCellRenderer'; - -class TextCellRenderer extends CellRenderer implements TokenBoxCellRenderer { - drawCellContent( - context: CanvasRenderingContext2D, - state: GridRenderState, - column: VisibleIndex, - row: VisibleIndex - ): void { - const { metrics, model, theme } = state; - const { - fontWidths, - modelColumns, - modelRows, - allRowHeights, - firstColumn, - } = metrics; - const isFirstColumn = column === firstColumn; - const { textColor } = theme; - const rowHeight = getOrThrow(allRowHeights, row); - const modelRow = getOrThrow(modelRows, row); - const modelColumn = getOrThrow(modelColumns, column); - const text = model.textForCell(modelColumn, modelRow); - const truncationChar = model.truncationCharForCell(modelColumn, modelRow); - - if (text && rowHeight > 0) { - const textAlign = model.textAlignForCell(modelColumn, modelRow) || 'left'; - context.textAlign = textAlign; - - const color = - model.colorForCell(modelColumn, modelRow, theme) || textColor; - context.fillStyle = color; - - context.save(); - - const { - width: textWidth, - x: textX, - y: textY, - } = GridUtils.getTextRenderMetrics(state, column, row); - - const fontWidth = fontWidths.get(context.font) ?? DEFAULT_FONT_WIDTH; - const truncatedText = this.getCachedTruncatedString( - context, - text, - textWidth, - fontWidth, - truncationChar - ); - - const tokens = model.tokensForCell( - modelColumn, - modelRow, - truncatedText.length - ); - - if (truncatedText) { - let tokenIndex = 0; - let textStart = 0; - let left = textX; - const { actualBoundingBoxDescent } = context.measureText(truncatedText); - - while (textStart < truncatedText.length) { - const nextToken = tokens[tokenIndex]; - const token = textStart === nextToken?.start ? nextToken : null; - const textEnd = - token?.end ?? nextToken?.start ?? truncatedText.length; - const value = truncatedText.substring(textStart, textEnd); - const { width } = context.measureText(value); - const widthOfUnderline = value.endsWith('…') - ? context.measureText(value.substring(0, value.length - 1)).width - : width; - - // Set the styling based on the token, then draw the text - if (token != null) { - context.fillStyle = theme.hyperlinkColor; - context.fillText(value, left, textY); - context.fillRect( - left, - textY + actualBoundingBoxDescent, - widthOfUnderline, - 1 - ); - } else { - context.fillStyle = color; - context.fillText(value, left, textY); - } - - left += width; - textStart = textEnd; - if (token != null) tokenIndex += 1; - } - } - context.restore(); - } - - if ( - isFirstColumn && - isExpandableGridModel(model) && - model.hasExpandableRows - ) { - this.drawCellRowTreeMarker(context, state, row); - } - } - - /** - * Gets the token boxes that are visible in the cell - * @param column The visible column - * @param row The visible row - * @param state The GridRenderState - * @returns An array of TokenBox of visible tokens or empty array with coordinates relative to gridX and gridY - */ - getTokenBoxesForVisibleCell( - column: VisibleIndex, - row: VisibleIndex, - state: GridRenderState - ): TokenBox[] { - const { metrics, context, model, theme } = state; - - if (context == null || metrics == null) { - return (EMPTY_ARRAY as unknown) as TokenBox[]; - } - - const { modelRows, modelColumns } = metrics; - const modelRow = getOrThrow(modelRows, row); - const modelColumn = getOrThrow(modelColumns, column); - - const text = model.textForCell(modelColumn, modelRow); - const { - width: textWidth, - x: textX, - y: textY, - } = GridUtils.getTextRenderMetrics(state, column, row); - - const { fontWidths } = metrics; - - // Set the font and baseline and change it back after - context.save(); - this.configureContext(context, state); - - const fontWidth = fontWidths?.get(context.font) ?? DEFAULT_FONT_WIDTH; - const truncationChar = model.truncationCharForCell(modelColumn, modelRow); - const truncatedText = this.getCachedTruncatedString( - context, - text, - textWidth, - fontWidth, - truncationChar - ); - - const { - actualBoundingBoxAscent, - actualBoundingBoxDescent, - } = context.measureText(truncatedText); - const textHeight = actualBoundingBoxAscent + actualBoundingBoxDescent; - - const tokens = model.tokensForCell( - modelColumn, - modelRow, - truncatedText.length - ); - - // Check if the truncated text contains a link - if (tokens.length === 0) { - context.restore(); - return (EMPTY_ARRAY as unknown) as TokenBox[]; - } - - const cachedTokenBoxes = this.getCachedTokenBoxesForVisibleCell( - truncatedText, - tokens, - theme.font, - 'middle', - textHeight, - context - ).map(tokenBox => ({ - x1: tokenBox.x1 + textX, - y1: tokenBox.y1 + (textY - actualBoundingBoxAscent), - x2: tokenBox.x2 + textX, - y2: tokenBox.y2 + (textY - actualBoundingBoxAscent), - token: tokenBox.token, - })); - - context.restore(); - - return cachedTokenBoxes; - } - - configureContext( - context: CanvasRenderingContext2D, - state: GridRenderState - ): void { - const { theme } = state; - context.font = theme.font; - context.textBaseline = 'middle'; - context.lineCap = 'butt'; - } - - /** - * Returns an array of token boxes with the coordinates relative to the top left corner of the text - */ - getCachedTokenBoxesForVisibleCell = memoizeClear( - ( - truncatedText: string, - tokens: Token[], - // _font and _baseline are passed in so value is re-calculated when they change - // They should already be set on the `context`, so they are not used in this method - _font: string, - _baseline: CanvasTextBaseline, - textHeight: number, - context: CanvasRenderingContext2D - ): TokenBox[] => { - const top = 0; - const bottom = textHeight; - - const tokenBoxes: TokenBox[] = []; - - // The index where the last token ended - let lastTokenEnd = 0; - // The width of the text preceding the current token - let currentTextWidth = 0; - // Loop through array and push them to array - for (let i = 0; i < tokens.length; i += 1) { - const token = tokens[i]; - const { start, end } = token; - // The last token value is calculated based on the full text so the value needs to be truncated - const value = - end > truncatedText.length - ? truncatedText.substring(start) - : token.value; - - // Add the width of the text in between this token and the last token - currentTextWidth += context.measureText( - truncatedText.substring(lastTokenEnd, start) - ).width; - const tokenWidth = context.measureText(value).width; - - // Check if the x position is less than the grid x, then tokenWidth should be shifted by gridX - startX - - const left = currentTextWidth; - const right = left + tokenWidth; - - const newTokenBox: TokenBox = { - x1: left, - y1: top, - x2: right, - y2: bottom, - token, - }; - - tokenBoxes.push(newTokenBox); - - lastTokenEnd = end; - currentTextWidth += tokenWidth; - } - - return tokenBoxes; - }, - { max: 10000 } - ); -} - -export default TextCellRenderer; diff --git a/packages/grid/src/TokenBoxCellRenderer.ts b/packages/grid/src/TokenBoxCellRenderer.ts deleted file mode 100644 index 834205e82e..0000000000 --- a/packages/grid/src/TokenBoxCellRenderer.ts +++ /dev/null @@ -1,23 +0,0 @@ -import CellRenderer from './CellRenderer'; -import { VisibleIndex } from './GridMetrics'; -import { GridRenderState } from './GridRendererTypes'; -import { TokenBox } from './GridUtils'; - -export function isTokenBoxCellRenderer( - cellRenderer: CellRenderer -): cellRenderer is TokenBoxCellRenderer { - return ( - (cellRenderer as TokenBoxCellRenderer)?.getTokenBoxesForVisibleCell !== - undefined - ); -} - -interface TokenBoxCellRenderer extends CellRenderer { - getTokenBoxesForVisibleCell( - column: VisibleIndex, - row: VisibleIndex, - state: GridRenderState - ): TokenBox[]; -} - -export default TokenBoxCellRenderer; diff --git a/packages/grid/src/index.ts b/packages/grid/src/index.ts index 0ba0217031..8ea272b666 100644 --- a/packages/grid/src/index.ts +++ b/packages/grid/src/index.ts @@ -20,14 +20,9 @@ export { default as MockTreeGridModel } from './MockTreeGridModel'; export { default as memoizeClear } from './memoizeClear'; export { default as StaticDataGridModel } from './StaticDataGridModel'; export { default as ViewportDataGridModel } from './ViewportDataGridModel'; -export { default as MockDataBarGridModel } from './MockDataBarGridModel'; export * from './key-handlers'; export * from './mouse-handlers'; export * from './errors'; export * from './EventHandlerResult'; export { default as ThemeContext } from './ThemeContext'; -export type { default as CellRenderer, CellRenderType } from './CellRenderer'; -export { default as TextCellRenderer } from './TextCellRenderer'; -export { default as DataBarCellRenderer } from './DataBarCellRenderer'; -export * from './TokenBoxCellRenderer'; export * from './GridRendererTypes'; diff --git a/packages/grid/src/mouse-handlers/GridTokenMouseHandler.ts b/packages/grid/src/mouse-handlers/GridTokenMouseHandler.ts index 648aa5aee4..09ed028a89 100644 --- a/packages/grid/src/mouse-handlers/GridTokenMouseHandler.ts +++ b/packages/grid/src/mouse-handlers/GridTokenMouseHandler.ts @@ -1,13 +1,11 @@ /* eslint class-methods-use-this: "off" */ -import { getOrThrow } from '@deephaven/utils'; import { isEditableGridModel } from '../EditableGridModel'; import { EventHandlerResult } from '../EventHandlerResult'; import Grid from '../Grid'; import GridMouseHandler, { GridMouseEvent } from '../GridMouseHandler'; import GridRange from '../GridRange'; import GridUtils, { GridPoint, isLinkToken, TokenBox } from '../GridUtils'; -import { isTokenBoxCellRenderer } from '../TokenBoxCellRenderer'; class GridTokenMouseHandler extends GridMouseHandler { timeoutId?: ReturnType; @@ -23,24 +21,13 @@ class GridTokenMouseHandler extends GridMouseHandler { isHoveringLink(gridPoint: GridPoint, grid: Grid): boolean { const { column, row, x, y } = gridPoint; - const { renderer, metrics, props } = grid; - const { model } = props; + const { renderer, metrics } = grid; if (column == null || row == null || metrics == null) { this.currentLinkBox = undefined; return false; } - const { modelRows, modelColumns } = metrics; - const modelRow = getOrThrow(modelRows, row); - const modelColumn = getOrThrow(modelColumns, column); - - const renderType = model.renderTypeForCell(modelColumn, modelRow); - const cellRenderer = renderer.getCellRenderer(renderType); - if (!isTokenBoxCellRenderer(cellRenderer)) { - return false; - } - if (this.currentLinkBox != null) { const { x1: left, y1: top, x2: right, y2: bottom } = this.currentLinkBox; if (x >= left && x <= right && y >= top && y <= bottom) { @@ -49,8 +36,7 @@ class GridTokenMouseHandler extends GridMouseHandler { } const renderState = grid.updateRenderState(); - - const tokensInCell = cellRenderer.getTokenBoxesForVisibleCell( + const tokensInCell = renderer.getTokenBoxesForVisibleCell( column, row, renderState diff --git a/packages/iris-grid/src/IrisGridCellRendererUtils.ts b/packages/iris-grid/src/IrisGridCellRendererUtils.ts deleted file mode 100644 index 3b322fd99a..0000000000 --- a/packages/iris-grid/src/IrisGridCellRendererUtils.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { BoxCoordinates, Coordinate } from '@deephaven/grid'; -import { getIcon } from './IrisGridIcons'; -import { IrisGridRenderState } from './IrisGridRenderer'; - -class IrisGridCellRendererUtils { - static drawTreeMarker( - context: CanvasRenderingContext2D, - state: IrisGridRenderState, - columnX: Coordinate, - rowY: Coordinate, - treeBox: BoxCoordinates, - color: string, - isExpanded: boolean - ): void { - context.save(); - const { x1, y1 } = treeBox; - const markerIcon = isExpanded - ? getIcon('caretDown') - : getIcon('caretRight'); - const iconX = columnX + x1 - 2; - const iconY = rowY + y1 + 2.5; - - context.fillStyle = color; - context.textAlign = 'center'; - context.translate(iconX, iconY); - context.fill(markerIcon); - context.restore(); - } -} - -export default IrisGridCellRendererUtils; diff --git a/packages/iris-grid/src/IrisGridDataBarCellRenderer.ts b/packages/iris-grid/src/IrisGridDataBarCellRenderer.ts deleted file mode 100644 index d70239491e..0000000000 --- a/packages/iris-grid/src/IrisGridDataBarCellRenderer.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* eslint-disable class-methods-use-this */ -import { - BoxCoordinates, - Coordinate, - DataBarCellRenderer, -} from '@deephaven/grid'; -import { IrisGridRenderState } from './IrisGridRenderer'; -import IrisGridCellRendererUtils from './IrisGridCellRendererUtils'; - -class IrisGridDataBarCellRenderer extends DataBarCellRenderer { - drawTreeMarker( - context: CanvasRenderingContext2D, - state: IrisGridRenderState, - columnX: Coordinate, - rowY: Coordinate, - treeBox: BoxCoordinates, - color: string, - isExpanded: boolean - ): void { - IrisGridCellRendererUtils.drawTreeMarker( - context, - state, - columnX, - rowY, - treeBox, - color, - isExpanded - ); - } -} - -export default IrisGridDataBarCellRenderer; diff --git a/packages/iris-grid/src/IrisGridIcons.ts b/packages/iris-grid/src/IrisGridIcons.ts deleted file mode 100644 index 9dcd58a46f..0000000000 --- a/packages/iris-grid/src/IrisGridIcons.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { memoizeClear } from '@deephaven/grid'; -import { - dhSortDown, - dhSortUp, - vsTriangleDown, - vsTriangleRight, - vsLinkExternal, - IconDefinition, -} from '@deephaven/icons'; - -export const ICON_SIZE = 16; - -export type IconName = - | 'sortUp' - | 'sortDown' - | 'caretDown' - | 'caretRight' - | 'cellOverflow'; - -const iconMap = new Map([ - ['sortUp', dhSortUp], - ['sortDown', dhSortDown], - ['caretDown', vsTriangleDown], - ['caretRight', vsTriangleRight], - ['cellOverflow', vsLinkExternal], -]); - -const makeIcon = memoizeClear( - (name: IconName) => { - const faIcon = iconMap.get(name); - if (faIcon === undefined) { - throw new Error('Icon is undefined'); - } - - const path = Array.isArray(faIcon.icon[4]) - ? faIcon.icon[4][0] - : faIcon.icon[4]; - const icon = new Path2D(path); - const scaledIcon = new Path2D(); - const scaleMatrix = { - a: ICON_SIZE / faIcon.icon[0], - d: ICON_SIZE / faIcon.icon[1], - }; - scaledIcon.addPath(icon, scaleMatrix); - return scaledIcon; - }, - { max: 1000 } -); - -export function getIcon(name: IconName): Path2D { - return makeIcon(name); -} diff --git a/packages/iris-grid/src/IrisGridRenderer.ts b/packages/iris-grid/src/IrisGridRenderer.ts index 9dcb1b3ebc..83da0d4708 100644 --- a/packages/iris-grid/src/IrisGridRenderer.ts +++ b/packages/iris-grid/src/IrisGridRenderer.ts @@ -1,10 +1,18 @@ /* eslint react/destructuring-assignment: "off" */ /* eslint class-methods-use-this: "off" */ /* eslint no-param-reassign: "off" */ +import { + dhSortDown, + dhSortUp, + vsTriangleDown, + vsTriangleRight, + vsLinkExternal, + IconDefinition, +} from '@deephaven/icons'; import { BoundedAxisRange, + BoxCoordinates, Coordinate, - DEFAULT_FONT_WIDTH, GridMetrics, GridRangeIndex, GridRenderer, @@ -24,9 +32,6 @@ import { } from './CommonTypes'; import { IrisGridThemeType } from './IrisGridTheme'; import IrisGridModel from './IrisGridModel'; -import IrisGridTextCellRenderer from './IrisGridTextCellRenderer'; -import IrisGridDataBarCellRenderer from './IrisGridDataBarCellRenderer'; -import { getIcon } from './IrisGridIcons'; const ICON_NAMES = Object.freeze({ SORT_UP: 'sortUp', @@ -37,6 +42,7 @@ const ICON_NAMES = Object.freeze({ }); const EXPAND_ICON_SIZE = 10; +const ICON_SIZE = 16; export type IrisGridRenderState = GridRenderState & { model: IrisGridModel; @@ -65,19 +71,51 @@ class IrisGridRenderer extends GridRenderer { return isAdvancedFilterValid && isQuickFilterValid; } - protected textCellRenderer = new IrisGridTextCellRenderer(); + constructor() { + super(); + this.icons = {}; + + this.initIcons(); + } + + icons: Record; + + initIcons(): void { + this.setIcon(ICON_NAMES.SORT_UP, dhSortUp); + this.setIcon(ICON_NAMES.SORT_DOWN, dhSortDown); + this.setIcon(ICON_NAMES.CARET_DOWN, vsTriangleDown); + this.setIcon(ICON_NAMES.CARET_RIGHT, vsTriangleRight); + this.setIcon(ICON_NAMES.CELL_OVERFLOW, vsLinkExternal); + } + + // Scales the icon to be square and match the global ICON_SIZE + setIcon(name: string, faIcon: IconDefinition): void { + const path = Array.isArray(faIcon.icon[4]) + ? faIcon.icon[4][0] + : faIcon.icon[4]; + const icon = new Path2D(path); + const scaledIcon = new Path2D(); + const scaleMatrix = { + a: ICON_SIZE / faIcon.icon[0], + d: ICON_SIZE / faIcon.icon[1], + }; + scaledIcon.addPath(icon, scaleMatrix); + this.icons[name] = scaledIcon; + } - protected dataBarCellRenderer = new IrisGridDataBarCellRenderer(); + getIcon(name: string): Path2D { + return this.icons[name]; + } getSortIcon(sort: Sort | null): Path2D | null { if (!sort) { return null; } if (sort.direction === TableUtils.sortDirection.ascending) { - return getIcon(ICON_NAMES.SORT_UP); + return this.getIcon(ICON_NAMES.SORT_UP); } if (sort.direction === TableUtils.sortDirection.descending) { - return getIcon(ICON_NAMES.SORT_DOWN); + return this.getIcon(ICON_NAMES.SORT_DOWN); } return null; } @@ -115,89 +153,23 @@ class IrisGridRenderer extends GridRenderer { ): void { const { metrics, model } = state; const { modelColumns, modelRows } = metrics; - const modelColumn = modelColumns.get(column); const modelRow = getOrThrow(modelRows, row); + const modelColumn = modelColumns.get(column); if (modelColumn === undefined) { return; } - - const renderType = model.renderTypeForCell(modelColumn, modelRow); - const cellRenderer = this.getCellRenderer(renderType); - cellRenderer.drawCellContent(context, state, column, row); - } - - getCellOverflowButtonPosition({ - mouseX, - mouseY, - metrics, - theme, - }: { - mouseX: Coordinate | null; - mouseY: Coordinate | null; - metrics: GridMetrics | undefined; - theme: GridThemeType; - }): { - left: Coordinate | null; - top: Coordinate | null; - width: number | null; - height: number | null; - } { - return this.textCellRenderer.getCellOverflowButtonPosition( - mouseX, - mouseY, - metrics, - theme - ); - } - - shouldRenderOverflowButton(state: IrisGridRenderState): boolean { - return this.textCellRenderer.shouldRenderOverflowButton(state); - } - - drawCellOverflowButton(state: IrisGridRenderState): void { - const { context, mouseX, mouseY, theme } = state; - if (mouseX == null || mouseY == null) return; - - if (!this.shouldRenderOverflowButton(state)) { - return; - } - - const { - left: buttonLeft, - top: buttonTop, - width: buttonWidth, - height: buttonHeight, - } = this.getCellOverflowButtonPosition(state); - - const { - cellHorizontalPadding, - overflowButtonColor, - overflowButtonHoverColor, - } = theme; - - context.save(); - if ( - overflowButtonHoverColor != null && - buttonLeft != null && - buttonWidth != null && - buttonTop != null && - buttonHeight != null && - mouseX >= buttonLeft && - mouseX <= buttonLeft + buttonWidth && - mouseY >= buttonTop && - mouseY <= buttonTop + buttonHeight - ) { - context.fillStyle = overflowButtonHoverColor; - } else if (overflowButtonColor != null) { - context.fillStyle = overflowButtonColor; - } - const icon = getIcon(ICON_NAMES.CELL_OVERFLOW); - if (buttonLeft != null && buttonTop != null) { - context.translate(buttonLeft + cellHorizontalPadding, buttonTop + 2); + const value = model.valueForCell(modelColumn, modelRow); + if (TableUtils.isTextType(model.columns[modelColumn]?.type)) { + if (value === null || value === '') { + const originalFont = context.font; + context.font = `italic ${originalFont}`; + const displayValue = value === null ? 'null' : 'empty'; + super.drawCellContent(context, state, column, row, displayValue); + context.font = originalFont; + return; + } } - context.fill(icon); - - context.restore(); + super.drawCellContent(context, state, column, row); } drawGroupedColumnLine( @@ -501,7 +473,8 @@ class IrisGridRenderer extends GridRenderer { return; } - const fontWidth = fontWidths.get(context.font) ?? DEFAULT_FONT_WIDTH; + const fontWidth = + fontWidths.get(context.font) ?? GridRenderer.DEFAULT_FONT_WIDTH; assertNotNull(fontWidth); const textWidth = text.length * fontWidth; const textRight = gridX + columnX + textWidth + headerHorizontalPadding; @@ -810,6 +783,30 @@ class IrisGridRenderer extends GridRenderer { context.restore(); } + drawTreeMarker( + context: CanvasRenderingContext2D, + state: IrisGridRenderState, + columnX: Coordinate, + rowY: Coordinate, + treeBox: BoxCoordinates, + color: string, + isExpanded: boolean + ): void { + context.save(); + const { x1, y1 } = treeBox; + const markerIcon = isExpanded + ? this.getIcon(ICON_NAMES.CARET_DOWN) + : this.getIcon(ICON_NAMES.CARET_RIGHT); + const iconX = columnX + x1 - 2; + const iconY = rowY + y1 + 2.5; + + context.fillStyle = color; + context.textAlign = 'center'; + context.translate(iconX, iconY); + context.fill(markerIcon); + context.restore(); + } + drawRowFooters( context: CanvasRenderingContext2D, state: IrisGridRenderState @@ -915,6 +912,174 @@ class IrisGridRenderer extends GridRenderer { context.translate(-gridX, -gridY); } + // This will shrink the size the text may take when the overflow button is rendered + // The text will truncate to a smaller width and won't overlap the button + getTextRenderMetrics( + state: IrisGridRenderState, + column: VisibleIndex, + row: VisibleIndex + ): { + width: number; + x: Coordinate; + y: Coordinate; + } { + const textMetrics = super.getTextRenderMetrics(state, column, row); + + const { mouseX, mouseY, metrics } = state; + + if (mouseX == null || mouseY == null) { + return textMetrics; + } + + const { column: mouseColumn, row: mouseRow } = GridUtils.getGridPointFromXY( + mouseX, + mouseY, + metrics + ); + + if (column === mouseColumn && row === mouseRow) { + const { left } = this.getCellOverflowButtonPosition(state); + if (this.shouldRenderOverflowButton(state) && left != null) { + textMetrics.width = left - metrics.gridX - textMetrics.x; + } + } + return textMetrics; + } + + shouldRenderOverflowButton(state: IrisGridRenderState): boolean { + const { context, mouseX, mouseY, metrics, model, theme } = state; + if (mouseX == null || mouseY == null) { + return false; + } + + const { row, column, modelRow, modelColumn } = GridUtils.getCellInfoFromXY( + mouseX, + mouseY, + metrics + ); + + if ( + row == null || + column == null || + modelRow == null || + modelColumn == null || + !TableUtils.isStringType(model.columns[modelColumn].type) + ) { + return false; + } + + const text = model.textForCell(modelColumn, modelRow) ?? ''; + const { width: textWidth } = super.getTextRenderMetrics(state, column, row); + const fontWidth = + metrics.fontWidths.get(theme.font) ?? IrisGridRenderer.DEFAULT_FONT_WIDTH; + + context.save(); + context.font = theme.font; + + const truncatedText = this.getCachedTruncatedString( + context, + text, + textWidth, + fontWidth, + model.truncationCharForCell(modelColumn, modelRow) + ); + context.restore(); + + return text !== '' && truncatedText !== text; + } + + getCellOverflowButtonPosition({ + mouseX, + mouseY, + metrics, + theme, + }: { + mouseX: Coordinate | null; + mouseY: Coordinate | null; + metrics: GridMetrics | undefined; + theme: GridThemeType; + }): { + left: Coordinate | null; + top: Coordinate | null; + width: number | null; + height: number | null; + } { + const NULL_POSITION = { left: null, top: null, width: null, height: null }; + if (mouseX == null || mouseY == null || metrics == null) { + return NULL_POSITION; + } + const { rowHeight, columnWidth, left, top } = GridUtils.getCellInfoFromXY( + mouseX, + mouseY, + metrics + ); + + if (left == null || columnWidth == null || top == null) { + return NULL_POSITION; + } + + const { width: gridWidth, verticalBarWidth } = metrics; + const { cellHorizontalPadding } = theme; + + const width = ICON_SIZE + 2 * cellHorizontalPadding; + const height = rowHeight; + // Right edge of column or of visible grid, whichever is smaller + const right = Math.min( + metrics.gridX + left + columnWidth, + gridWidth - verticalBarWidth + ); + const buttonLeft = right - width; + const buttonTop = metrics.gridY + top; + + return { left: buttonLeft, top: buttonTop, width, height }; + } + + drawCellOverflowButton(state: IrisGridRenderState): void { + const { context, mouseX, mouseY, theme } = state; + if (mouseX == null || mouseY == null) return; + + if (!this.shouldRenderOverflowButton(state)) { + return; + } + + const { + left: buttonLeft, + top: buttonTop, + width: buttonWidth, + height: buttonHeight, + } = this.getCellOverflowButtonPosition(state); + + const { + cellHorizontalPadding, + overflowButtonColor, + overflowButtonHoverColor, + } = theme; + + context.save(); + if ( + overflowButtonHoverColor != null && + buttonLeft != null && + buttonWidth != null && + buttonTop != null && + buttonHeight != null && + mouseX >= buttonLeft && + mouseX <= buttonLeft + buttonWidth && + mouseY >= buttonTop && + mouseY <= buttonTop + buttonHeight + ) { + context.fillStyle = overflowButtonHoverColor; + } else if (overflowButtonColor != null) { + context.fillStyle = overflowButtonColor; + } + const icon = this.getIcon(ICON_NAMES.CELL_OVERFLOW); + if (buttonLeft != null && buttonTop != null) { + context.translate(buttonLeft + cellHorizontalPadding, buttonTop + 2); + } + context.fill(icon); + + context.restore(); + } + getExpandButtonPosition( { mouseX, diff --git a/packages/iris-grid/src/IrisGridTableModelTemplate.ts b/packages/iris-grid/src/IrisGridTableModelTemplate.ts index 43bde731f8..35375708b4 100644 --- a/packages/iris-grid/src/IrisGridTableModelTemplate.ts +++ b/packages/iris-grid/src/IrisGridTableModelTemplate.ts @@ -543,17 +543,6 @@ class IrisGridTableModelTemplate< return '*'; } } - - if (TableUtils.isTextType(this.columns[x]?.type)) { - if (text === null) { - return 'null'; - } - - if (text === '') { - return 'empty'; - } - } - return text ?? ''; } diff --git a/packages/iris-grid/src/IrisGridTextCellRenderer.ts b/packages/iris-grid/src/IrisGridTextCellRenderer.ts deleted file mode 100644 index e243b258e8..0000000000 --- a/packages/iris-grid/src/IrisGridTextCellRenderer.ts +++ /dev/null @@ -1,191 +0,0 @@ -/* eslint-disable class-methods-use-this */ -import { - BoxCoordinates, - Coordinate, - DEFAULT_FONT_WIDTH, - getOrThrow, - GridMetrics, - GridThemeType, - GridUtils, - TextCellRenderer, - VisibleIndex, -} from '@deephaven/grid'; -import { TableUtils } from '@deephaven/jsapi-utils'; -import { IrisGridRenderState } from './IrisGridRenderer'; -import { ICON_SIZE } from './IrisGridIcons'; -import IrisGridCellRendererUtils from './IrisGridCellRendererUtils'; - -class IrisGridTextCellRenderer extends TextCellRenderer { - drawCellContent( - context: CanvasRenderingContext2D, - state: IrisGridRenderState, - column: VisibleIndex, - row: VisibleIndex - ): void { - const { metrics, model } = state; - const { modelColumns, modelRows } = metrics; - const modelRow = getOrThrow(modelRows, row); - const modelColumn = modelColumns.get(column); - if (modelColumn === undefined) { - return; - } - const value = model.valueForCell(modelColumn, modelRow); - if (TableUtils.isTextType(model.columns[modelColumn]?.type)) { - if (value === null || value === '') { - const originalFont = context.font; - context.font = `italic ${originalFont}`; - super.drawCellContent(context, state, column, row); - context.font = originalFont; - return; - } - } - super.drawCellContent(context, state, column, row); - } - - // This will shrink the size the text may take when the overflow button is rendered - // The text will truncate to a smaller width and won't overlap the button - getTextRenderMetrics( - state: IrisGridRenderState, - column: VisibleIndex, - row: VisibleIndex - ): { - width: number; - x: Coordinate; - y: Coordinate; - } { - const textMetrics = GridUtils.getTextRenderMetrics(state, column, row); - - const { mouseX, mouseY, metrics, theme } = state; - - if (mouseX == null || mouseY == null) { - return textMetrics; - } - - const { column: mouseColumn, row: mouseRow } = GridUtils.getGridPointFromXY( - mouseX, - mouseY, - metrics - ); - - if (column === mouseColumn && row === mouseRow) { - const { left } = this.getCellOverflowButtonPosition( - mouseX, - mouseY, - metrics, - theme - ); - if (this.shouldRenderOverflowButton(state) && left != null) { - textMetrics.width = left - metrics.gridX - textMetrics.x; - } - } - return textMetrics; - } - - getCellOverflowButtonPosition( - mouseX: Coordinate | null, - mouseY: Coordinate | null, - metrics: GridMetrics | undefined, - theme: GridThemeType - ): { - left: Coordinate | null; - top: Coordinate | null; - width: number | null; - height: number | null; - } { - const NULL_POSITION = { left: null, top: null, width: null, height: null }; - if (mouseX == null || mouseY == null || metrics == null) { - return NULL_POSITION; - } - const { rowHeight, columnWidth, left, top } = GridUtils.getCellInfoFromXY( - mouseX, - mouseY, - metrics - ); - - if (left == null || columnWidth == null || top == null) { - return NULL_POSITION; - } - - const { width: gridWidth, verticalBarWidth } = metrics; - const { cellHorizontalPadding } = theme; - - const width = ICON_SIZE + 2 * cellHorizontalPadding; - const height = rowHeight; - // Right edge of column or of visible grid, whichever is smaller - const right = Math.min( - metrics.gridX + left + columnWidth, - gridWidth - verticalBarWidth - ); - const buttonLeft = right - width; - const buttonTop = metrics.gridY + top; - - return { left: buttonLeft, top: buttonTop, width, height }; - } - - shouldRenderOverflowButton(state: IrisGridRenderState): boolean { - const { context, mouseX, mouseY, metrics, model, theme } = state; - if (mouseX == null || mouseY == null) { - return false; - } - - const { row, column, modelRow, modelColumn } = GridUtils.getCellInfoFromXY( - mouseX, - mouseY, - metrics - ); - - if ( - row == null || - column == null || - modelRow == null || - modelColumn == null || - !TableUtils.isStringType(model.columns[modelColumn].type) - ) { - return false; - } - - const text = model.textForCell(modelColumn, modelRow) ?? ''; - const { width: textWidth } = GridUtils.getTextRenderMetrics( - state, - column, - row - ); - const fontWidth = metrics.fontWidths.get(theme.font) ?? DEFAULT_FONT_WIDTH; - - context.save(); - context.font = theme.font; - - const truncatedText = this.getCachedTruncatedString( - context, - text, - textWidth, - fontWidth, - model.truncationCharForCell(modelColumn, modelRow) - ); - context.restore(); - - return text !== '' && truncatedText !== text; - } - - drawTreeMarker( - context: CanvasRenderingContext2D, - state: IrisGridRenderState, - columnX: Coordinate, - rowY: Coordinate, - treeBox: BoxCoordinates, - color: string, - isExpanded: boolean - ): void { - IrisGridCellRendererUtils.drawTreeMarker( - context, - state, - columnX, - rowY, - treeBox, - color, - isExpanded - ); - } -} - -export default IrisGridTextCellRenderer; diff --git a/packages/iris-grid/src/IrisGridTheme.module.scss b/packages/iris-grid/src/IrisGridTheme.module.scss index 54781e9aea..06972a20f2 100644 --- a/packages/iris-grid/src/IrisGridTheme.module.scss +++ b/packages/iris-grid/src/IrisGridTheme.module.scss @@ -91,8 +91,4 @@ $header-height: 30px; overflow-button-color: $gray-300; overflow-button-hover-color: $gray-100; - - zero-line-color: $gray-500; - positive-bar-color: $green; - negative-bar-color: $red; } diff --git a/packages/iris-grid/src/IrisGridTheme.ts b/packages/iris-grid/src/IrisGridTheme.ts index 32f736da4d..5e6d3b8658 100644 --- a/packages/iris-grid/src/IrisGridTheme.ts +++ b/packages/iris-grid/src/IrisGridTheme.ts @@ -135,10 +135,6 @@ const theme: Partial = Object.freeze({ overflowButtonColor: IrisGridTheme['overflow-button-color'], overflowButtonHoverColor: IrisGridTheme['overflow-button-hover-color'], - - zeroLineColor: IrisGridTheme['zero-line-color'], - positiveBarColor: IrisGridTheme['positive-bar-color'], - negativeBarColor: IrisGridTheme['negative-bar-color'], }); export default theme; diff --git a/packages/iris-grid/src/mousehandlers/IrisGridTokenMouseHandler.ts b/packages/iris-grid/src/mousehandlers/IrisGridTokenMouseHandler.ts index 3c97c4538e..7e1a132a21 100644 --- a/packages/iris-grid/src/mousehandlers/IrisGridTokenMouseHandler.ts +++ b/packages/iris-grid/src/mousehandlers/IrisGridTokenMouseHandler.ts @@ -1,13 +1,11 @@ import { EventHandlerResult, - getOrThrow, Grid, GridMouseHandler, GridPoint, GridUtils, isLinkToken, TokenBox, - isTokenBoxCellRenderer, } from '@deephaven/grid'; import deepEqual from 'deep-equal'; import IrisGrid from '../IrisGrid'; @@ -30,24 +28,13 @@ class IrisGridTokenMouseHandler extends GridMouseHandler { isHoveringLink(gridPoint: GridPoint, grid: Grid): boolean { const { column, row, x, y } = gridPoint; - const { renderer, metrics, props } = grid; - const { model } = props; + const { renderer, metrics } = grid; if (column == null || row == null || metrics == null) { this.currentLinkBox = undefined; return false; } - const { modelRows, modelColumns } = metrics; - const modelRow = getOrThrow(modelRows, row); - const modelColumn = getOrThrow(modelColumns, column); - - const renderType = model.renderTypeForCell(modelColumn, modelRow); - const cellRenderer = renderer.getCellRenderer(renderType); - if (!isTokenBoxCellRenderer(cellRenderer)) { - return false; - } - if (this.currentLinkBox != null) { const { x1: left, y1: top, x2: right, y2: bottom } = this.currentLinkBox; if (x >= left && x <= right && y >= top && y <= bottom) { @@ -56,7 +43,7 @@ class IrisGridTokenMouseHandler extends GridMouseHandler { } const renderState = grid.updateRenderState(); - const tokensInCell = cellRenderer.getTokenBoxesForVisibleCell( + const tokensInCell = renderer.getTokenBoxesForVisibleCell( column, row, renderState