Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion packages/core/components/DataTable/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import isRightAlignedTableValue from '@cdc/core/helpers/isRightAlignedTableValue
import './data-table.css'
import _ from 'lodash'
import { getDataSeriesColumns } from './helpers/getDataSeriesColumns'
import { getRowColors } from './helpers/getRowColors'

export type DataTableProps = {
colorScale?: Function
Expand Down Expand Up @@ -57,6 +58,7 @@ export type DataTableProps = {

const DataTable = (props: DataTableProps) => {
const {
colorScale,
columns,
config,
dataConfig,
Expand Down Expand Up @@ -275,6 +277,9 @@ const DataTable = (props: DataTableProps) => {
return classes
}

// Create sorted runtime data to match the row order for color calculations
const sortedRuntimeData = rows.map(rowKey => runtimeData[rowKey]).filter(Boolean)

const childrenMatrix =
config.type === 'map'
? mapCellMatrix({ ...props, rows, wrapColumns, runtimeData, viewport })
Expand All @@ -283,7 +288,7 @@ const DataTable = (props: DataTableProps) => {
const useBottomExpandCollapse = config.table.showBottomCollapse && expanded && Array.isArray(childrenMatrix)

// If every value in a column is a number, record the column index so the header and cells can be right-aligned
const rightAlignedCols = childrenMatrix.length
const rightAlignedCols = Array.isArray(childrenMatrix) && childrenMatrix.length
? Object.fromEntries(
Object.keys(childrenMatrix[0])
.filter(
Expand All @@ -293,6 +298,30 @@ const DataTable = (props: DataTableProps) => {
)
: {}

// Calculate row colors if row coloring is enabled
const getRowColor = useMemo(() => {
return config.table?.rowColors?.enabled
? getRowColors(
runtimeData,
config.table.rowColors,
'v2',
colorScale,
config.general?.palette?.name, // Use general palette if no table-specific palette
config.table.rowColors?.customRange
)
: () => undefined
}, [
config.table?.rowColors?.enabled,
config.table?.rowColors?.colorColumn,
config.table?.rowColors?.mode,
config.table?.rowColors?.palette,
config.table?.rowColors?.customRange,
config.general?.palette?.name, // Add this so it updates when palette changes
runtimeData,
colorScale
])


const showCollapseButton = config.table.collapsible !== false && useBottomExpandCollapse
const TableMediaControls = ({ belowTable }) => {
const hasDownloadLink = config.table.download
Expand Down Expand Up @@ -331,6 +360,7 @@ const DataTable = (props: DataTableProps) => {
)}
<div className='table-container' style={limitHeight}>
<Table
key={`table-${config.general?.palette?.name}-${config.table?.rowColors?.palette}`}
preliminaryData={config.preliminaryData}
viewport={viewport}
wrapColumns={wrapColumns}
Expand Down Expand Up @@ -374,6 +404,9 @@ const DataTable = (props: DataTableProps) => {
cellMinWidth: 100
}}
rightAlignedCols={rightAlignedCols}
getRowColor={getRowColor}
colorColumn={config.table?.rowColors?.colorColumn}
runtimeData={sortedRuntimeData}
/>

{/* REGION Data Table */}
Expand Down
125 changes: 125 additions & 0 deletions packages/core/components/DataTable/helpers/getRowColors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { RowColorConfig } from '../../../types/Table'
import { chartColorPalettes } from '../../../data/chartColorPalettes'
import chroma from 'chroma-js'

type RowColorResult = {
backgroundColor: string
textColor: string
}

export const getRowColors = (
data: Record<string, any>[],
rowColorConfig: RowColorConfig,
paletteVersion: 'v1' | 'v2' = 'v2',
existingColorScale?: Function,
tablePalette?: string,
customRange?: { min: number; max: number }
): (value: any) => RowColorResult | undefined => {
// Return a function that calculates color based on the actual value
// instead of a Map keyed by row index

if (!rowColorConfig?.enabled || !rowColorConfig.colorColumn) {
return () => undefined
}

const { colorColumn, mode = 'sequential', customColors = {} } = rowColorConfig

let colorScale: (value: any) => string

// Always create our own color scale for data table row coloring
// (don't use chart colorScale as it's for series, not data values)
{
// Always use the general palette for table row colors
const paletteToUse = tablePalette || 'sequential_blue'
const paletteColors = chartColorPalettes[paletteVersion]?.[paletteToUse] || chartColorPalettes.v2.sequential_blue

switch (mode) {
case 'sequential':
// Extract numeric values for min/max calculation
const numericValues = data
.map(row => Number(row[colorColumn]))
.filter(val => !isNaN(val))

if (numericValues.length === 0) {
colorScale = () => paletteColors[0]
} else {
// Use custom range if provided, otherwise calculate from data
const min = customRange?.min ?? Math.min(...numericValues)
const max = customRange?.max ?? Math.max(...numericValues)

if (min === max) {
colorScale = () => paletteColors[0]
} else {
// For sequential palettes, use first and last colors for smooth gradient
const startColor = paletteColors[0]
const endColor = paletteColors[paletteColors.length - 1]
const chromaScale = chroma.scale([startColor, endColor]).domain([min, max])
colorScale = (value: any) => chromaScale(Number(value)).hex()
}
}
break

case 'categorical':
// For categorical data, assign colors cyclically
const values = data.map(row => row[colorColumn]).filter(val => val !== undefined && val !== null)
const uniqueValues = Array.from(new Set(values))
const valueToColorMap = new Map<any, string>()

uniqueValues.forEach((value, index) => {
valueToColorMap.set(value, paletteColors[index % paletteColors.length])
})

colorScale = (value: any) => valueToColorMap.get(value) || paletteColors[0]
break

case 'custom':
// Use custom color mapping
colorScale = (value: any) => customColors[value] || '#ffffff'
break

default:
colorScale = () => '#ffffff'
}
}

// Return a function that calculates color for any given value
return (value: any) => {
// Skip invalid values based on mode
let shouldColor = false
if (mode === 'sequential') {
// For sequential mode, only color numeric values
shouldColor = value !== undefined && value !== null && value !== '' &&
value !== 'N/A' && value !== 'n/a' && !isNaN(Number(value))
} else {
// For categorical and custom modes, allow any non-null/undefined values
shouldColor = value !== undefined && value !== null && value !== ''
}

if (!shouldColor) {
return undefined
}

const backgroundColor = colorScale(value)

// Only proceed if we got a valid color
if (!backgroundColor || typeof backgroundColor !== 'string') {
return undefined
}

try {
// Calculate contrasting text color
const textColor = chroma.contrast(backgroundColor, '#000000') > 4.5 ? '#000000' : '#ffffff'

return {
backgroundColor,
textColor
}
} catch (error) {
// Fallback to default colors
return {
backgroundColor: backgroundColor,
textColor: '#000000'
}
}
}
}
93 changes: 93 additions & 0 deletions packages/core/components/EditorPanel/DataTableEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
import { Visualization } from '../../types/Visualization'
import _ from 'lodash'
import { Column } from '../../types/Column'
import { chartColorPalettes } from '../../data/chartColorPalettes'

interface DataTableProps {
config: Partial<Visualization>
Expand All @@ -26,6 +27,28 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
.map(([key]) => key)
}, [config.columns])

// Helper function to update rowColors with proper state management
const updateRowColorsField = (_section: string, _subsection: string, fieldName: string, value: any) => {
// Start with existing rowColors or create a new object
const currentRowColors = config.table.rowColors || {} as any
const newRowColors = {
enabled: currentRowColors.enabled || false,
colorColumn: currentRowColors.colorColumn,
mode: currentRowColors.mode || 'sequential',
palette: currentRowColors.palette || 'sequential_blue',
customColors: currentRowColors.customColors,
...currentRowColors, // Preserve any other existing properties
[fieldName]: value // Update the specific field
}

// Auto-enable when selecting a color column
if (fieldName === 'colorColumn' && value !== '') {
newRowColors.enabled = true
}

updateField('table', null, 'rowColors', newRowColors as any)
}

const groupPivotColumns = useMemo(() => {
const columns: string[] = config.data.flatMap(Object.keys)
const cols = _.uniq(columns).filter(key => {
Expand Down Expand Up @@ -322,6 +345,76 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
}
/>
)}

{/* ROW COLORING SECTION */}
<CheckBox
value={config.table.rowColors?.enabled || false}
fieldName='enabled'
label='Enable Row Coloring'
section='table'
subsection='rowColors'
updateField={updateRowColorsField}
tooltip={
<Tooltip style={{ textTransform: 'none' }}>
<Tooltip.Target>
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
</Tooltip.Target>
<Tooltip.Content>
<p>Apply background colors to table rows based on data values</p>
</Tooltip.Content>
</Tooltip>
}
/>

{config.table.rowColors?.enabled && (
<>
<Select
value={config.table.rowColors?.colorColumn || ''}
fieldName='colorColumn'
section='table'
subsection='rowColors'
label='Color Column'
updateField={updateRowColorsField}
initial='-Select Column-'
options={dataColumns}
tooltip={
<Tooltip style={{ textTransform: 'none' }}>
<Tooltip.Target>
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
</Tooltip.Target>
<Tooltip.Content>
<p>Select the data column to base row colors on</p>
</Tooltip.Content>
</Tooltip>
}
/>

<Select
value={config.table.rowColors?.mode || 'sequential'}
fieldName='mode'
section='table'
subsection='rowColors'
label='Color Mode'
updateField={updateRowColorsField}
options={[
{ label: 'Sequential (Numeric)', value: 'sequential' },
{ label: 'Categorical (Discrete)', value: 'categorical' }
]}
tooltip={
<Tooltip style={{ textTransform: 'none' }}>
<Tooltip.Target>
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
</Tooltip.Target>
<Tooltip.Content>
<p>Sequential: Colors scale with numeric values. Categorical: Different color per unique value. Custom: Manual color mapping.</p>
</Tooltip.Content>
</Tooltip>
}
/>

</>
)}

<Select
label='Pivot Column'
tooltip={
Expand Down
12 changes: 11 additions & 1 deletion packages/core/components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ type TableProps = {
viewport: 'lg' | 'md' | 'sm' | 'xs' | 'xxs'
preliminaryData?: PreliminaryDataItem[]
rightAlignedCols: object
getRowColor?: (value: any) => { backgroundColor: string; textColor: string } | undefined
colorColumn?: string
runtimeData?: Record<string, any>[]
}

type Position = 'sticky'
Expand All @@ -41,7 +44,10 @@ const Table = ({
hasRowType,
viewport,
preliminaryData,
rightAlignedCols
rightAlignedCols,
getRowColor,
colorColumn,
runtimeData
}: TableProps) => {
const headStyle = stickyHeader ? { position: 'sticky' as Position, top: 0, zIndex: 2 } : {}
const isGroupedMatrix = !Array.isArray(childrenMatrix)
Expand Down Expand Up @@ -73,6 +79,7 @@ const Table = ({
cellMinWidth={tableOptions.cellMinWidth}
viewport={viewport}
rightAlignedCols={rightAlignedCols}
rowColor={rowColors?.get(i)}
/>
)
})
Expand All @@ -94,6 +101,7 @@ const Table = ({
cellMinWidth={tableOptions.cellMinWidth}
viewport={viewport}
rightAlignedCols={rightAlignedCols}
rowColor={getRowColor && colorColumn && runtimeData ? getRowColor(runtimeData[i]?.[colorColumn]) : undefined}
/>
)
} else {
Expand All @@ -112,6 +120,7 @@ const Table = ({
cellMinWidth={tableOptions.cellMinWidth}
viewport={viewport}
rightAlignedCols={rightAlignedCols}
rowColor={getRowColor && colorColumn && runtimeData ? getRowColor(runtimeData[i]?.[colorColumn]) : undefined}
/>
)
case RowType.row_group_total:
Expand All @@ -127,6 +136,7 @@ const Table = ({
cellMinWidth={tableOptions.cellMinWidth}
viewport={viewport}
rightAlignedCols={rightAlignedCols}
rowColor={getRowColor && colorColumn && runtimeData ? getRowColor(runtimeData[i]?.[colorColumn]) : undefined}
/>
)
}
Expand Down
Loading