import { Box, useTheme } from '@mui/material' import { useWidgetSelector } from '../stores/use-widget-selector' import { styles } from './style' import type { CategoryUIProps, CategoryWidgetState } from './types' import { CategoryRowSingle } from './components/category-row-single' import { CategoryRowMulti } from './components/category-row-multi' import { CategoryRowOther } from './components/category-row-other' import { CategoryLegend } from './components/category-legend' import { useState } from 'react' import { defaultFormatter, defaultLabelFormatter } from '../utils/formatter' import { useWidgetRef } from '../../hooks' /** * Renders a category widget displaying horizontal bars for categorical data with support for single and multi-series layouts, selection, and overflow grouping. */ export function CategoryUI({ id }: CategoryUIProps) { const { ref } = useWidgetRef(id) const theme = useTheme() // Single consolidated subscription instead of 9 separate ones. const { _formatter, _labelFormatter, _series, data, maxItems, labels, onRowClick, selected, max, } = useWidgetSelector(id, (w) => { const cw = w as CategoryWidgetState | undefined return { _formatter: cw?.formatter, _labelFormatter: cw?.labelFormatter, _series: cw?.series, data: cw?.data, maxItems: cw?.maxItems, labels: cw?.labels, onRowClick: cw?.onRowClick, selected: cw?.selected, max: cw?.max, } }) const formatter = _formatter ?? defaultFormatter const labelFormatter = _labelFormatter ?? defaultLabelFormatter const series = _series ?? [] const [maxHeight] = useState( maxItems ? 40 * (series.length || 1) * maxItems : undefined, ) const qualitativeColors = Object.values(theme.palette.qualitative.bold) const colors = series.length > 0 ? series.map( (s, index: number) => s.color ?? qualitativeColors[index % qualitativeColors.length] ?? theme.palette.secondary.main, ) : [theme.palette.secondary.main] // Group data items by name to support multi-series display // data is CategoryDataItem[][] where data[seriesIndex] contains items for that series const groupedData = generateGroupedData({ data }) if (groupedData.length === 0) { return null } const maxValue = max ?? Math.max(...groupedData.flatMap((item) => item.values)) // Slice data to maxItems and compute hidden count const visibleData = maxItems !== undefined && maxItems >= 0 ? groupedData.slice(0, maxItems) : groupedData const hiddenCount = groupedData.length - visibleData.length const isMulti = series.length > 1 return ( {isMulti ? visibleData.map((item) => ( )) : visibleData.map((item) => ( ))} {hiddenCount > 0 && ( )} {series.length > 0 && } ) } function generateGroupedData({ data, }: { data: { name: string; value: number }[][] | undefined }) { if (!data || data.length === 0) return [] const seriesCount = Math.max(data.length, 1) const grouped = new Map() const nameOrder: string[] = [] // Iterate over each series (outer array) for (let seriesIndex = 0; seriesIndex < data.length; seriesIndex++) { const seriesData = data[seriesIndex]! for (const item of seriesData) { let values = grouped.get(item.name) if (!values) { values = new Array(seriesCount).fill(0) grouped.set(item.name, values) nameOrder.push(item.name) } values[seriesIndex] = item.value } } return nameOrder.map((name) => ({ name, values: grouped.get(name)!, })) }