From 486f43e0babf5ed5d9b2076e35b0eb0b93b22f34 Mon Sep 17 00:00:00 2001 From: CodexNexor Date: Mon, 4 May 2026 13:24:03 +0530 Subject: [PATCH 1/3] Guard chart components against null streaming data --- .../AreaChartCondensed/AreaChartCondensed.tsx | 24 ++++++----- .../BarChartCondensed/BarChartCondensed.tsx | 30 +++++++------- .../HorizontalBarChart/HorizontalBarChart.tsx | 40 ++++++++++++------- .../LineChartCondensed/LineChartCondensed.tsx | 24 ++++++----- .../components/Charts/PieChart/PieChart.tsx | 7 ++-- .../Charts/RadarChart/RadarChart.tsx | 11 ++--- .../Charts/RadialChart/RadialChart.tsx | 7 ++-- .../Charts/ScatterChart/ScatterChart.tsx | 26 ++++++------ .../src/components/Charts/utils/dataUtils.ts | 4 ++ 9 files changed, 98 insertions(+), 75 deletions(-) diff --git a/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx b/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx index ea3f13eda..eb6f6faf0 100644 --- a/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx +++ b/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx @@ -27,6 +27,7 @@ import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip"; import { LegendItem } from "../types"; import { getLineType } from "../utils/AreaAndLine/common"; import { + ensureChartData, get2dChartConfig, getColorForDataKey, getDataKeys, @@ -81,28 +82,29 @@ const AreaChartCondensedComponent = ({ }: AreaChartCondensedProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; + const chartData = useMemo(() => ensureChartData(data), [data]); const dataKeys = useMemo(() => { - return getDataKeys(data, categoryKey as string); - }, [data, categoryKey]); + return getDataKeys(chartData, categoryKey as string); + }, [chartData, categoryKey]); const variant = getLineType(areaChartVariant); - const { yAxisWidth, setLabelWidth } = useYAxisLabelWidth(data, dataKeys); + const { yAxisWidth, setLabelWidth } = useYAxisLabelWidth(chartData, dataKeys); - const maxLabelWidth = useMaxLabelWidth(data, categoryKey as string); + const maxLabelWidth = useMaxLabelWidth(chartData, categoryKey as string); const chartContainerRef = useRef(null); const [chartContainerWidth, setChartContainerWidth] = useState(0); const widthOfData = useMemo(() => { - if (data.length === 0) { + if (chartData.length === 0) { return 0; } // Use passed width if available, otherwise use observed chartContainerWidth const chartWidth = width ?? chartContainerWidth; - return chartWidth / data.length; - }, [width, chartContainerWidth, data]); + return chartWidth / chartData.length; + }, [width, chartContainerWidth, chartData]); const { angle: calculatedAngle, height: xAxisHeight } = useAutoAngleCalculation( maxLabelWidth, @@ -139,7 +141,7 @@ const AreaChartCondensedComponent = ({ const exportData = useExportChartData({ type: "area", - data, + data: chartData, categoryKey: categoryKey as string, dataKeys, colors, @@ -240,7 +242,7 @@ const AreaChartCondensedComponent = ({ key={`y-axis-area-chart-condensed-${id}`} width={yAxisWidth} height={effectiveHeight} - data={data} + data={chartData} margin={{ top: chartMargin.top, bottom: xAxisHeight + chartMargin.bottom, // this is required to give space for x-axis @@ -273,7 +275,7 @@ const AreaChartCondensedComponent = ({ }, [ showYAxis, effectiveHeight, - data, + chartData, dataKeys, id, yAxisWidth, @@ -315,7 +317,7 @@ const AreaChartCondensedComponent = ({ diff --git a/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx b/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx index 4f5089cba..abe7f41c7 100644 --- a/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx +++ b/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx @@ -28,6 +28,7 @@ import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip"; import { LegendItem } from "../types"; import { getBarStackInfo, getRadiusArray } from "../utils/BarCharts/BarChartsUtils"; import { + ensureChartData, get2dChartConfig, getColorForDataKey, getDataKeys, @@ -95,26 +96,27 @@ const BarChartCondensedComponent = ({ }: BarChartCondensedProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; + const chartData = useMemo(() => ensureChartData(data), [data]); const dataKeys = useMemo(() => { - return getDataKeys(data, categoryKey as string); - }, [data, categoryKey]); + return getDataKeys(chartData, categoryKey as string); + }, [chartData, categoryKey]); - const { yAxisWidth, setLabelWidth } = useYAxisLabelWidth(data, dataKeys); + const { yAxisWidth, setLabelWidth } = useYAxisLabelWidth(chartData, dataKeys); - const maxLabelWidth = useMaxLabelWidth(data, categoryKey as string); + const maxLabelWidth = useMaxLabelWidth(chartData, categoryKey as string); const chartContainerRef = useRef(null); const [chartContainerWidth, setChartContainerWidth] = useState(0); const widthOfData = useMemo(() => { - if (data.length === 0) { + if (chartData.length === 0) { return 0; } // Use passed width if available, otherwise use observed chartContainerWidth const chartWidth = width ?? chartContainerWidth; - return chartWidth / data.length; - }, [chartContainerWidth, data, width]); + return chartWidth / chartData.length; + }, [chartContainerWidth, chartData, width]); const { angle: calculatedAngle, height: xAxisHeight } = useAutoAngleCalculation( maxLabelWidth, @@ -150,7 +152,7 @@ const BarChartCondensedComponent = ({ const exportData = useExportChartData({ type: "bar", - data, + data: chartData, categoryKey: categoryKey as string, dataKeys, colors, @@ -224,12 +226,12 @@ const BarChartCondensedComponent = ({ const availableWidth = explicitChartWidth ?? chartContainerWidth; // If no width available, return undefined and let Recharts auto-size - if (!availableWidth || availableWidth === 0 || data.length === 0) { + if (!availableWidth || availableWidth === 0 || chartData.length === 0) { return undefined; } // Calculate space per category (Recharts handles gaps automatically via barGap and barCategoryGap props) - const spacePerCategory = availableWidth / data.length; + const spacePerCategory = availableWidth / chartData.length; // For grouped charts, multiple bars share the category space const barsPerCategory = variant === "stacked" ? 1 : dataKeys.length; @@ -239,7 +241,7 @@ const BarChartCondensedComponent = ({ // Only apply maximum constraint, let Recharts handle thin bars automatically return Math.min(maxBarWidth, barWidth); - }, [explicitChartWidth, chartContainerWidth, data.length, dataKeys.length, variant, maxBarWidth]); + }, [explicitChartWidth, chartContainerWidth, chartData.length, dataKeys.length, variant, maxBarWidth]); // Handle mouse events for bar hovering const handleChartMouseMove = useCallback((state: any) => { @@ -325,7 +327,7 @@ const BarChartCondensedComponent = ({ key={`y-axis-bar-chart-condensed-${id}`} width={yAxisWidth} height={effectiveHeight} - data={data} + data={chartData} stackOffset="sign" margin={{ top: chartMargin.top, @@ -359,7 +361,7 @@ const BarChartCondensedComponent = ({ }, [ showYAxis, effectiveHeight, - data, + chartData, dataKeys, variant, id, @@ -464,7 +466,7 @@ const BarChartCondensedComponent = ({ stackOffset="sign" accessibilityLayer key={`bar-chart-condensed-${id}`} - data={data} + data={chartData} margin={chartMargin} barGap={BAR_GAP} barCategoryGap={BAR_CATEGORY_GAP} diff --git a/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx b/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx index 568279e19..520489198 100644 --- a/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx +++ b/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx @@ -27,6 +27,7 @@ import { getRadiusArray, } from "../utils/BarCharts/BarChartsUtils"; import { + ensureChartData, get2dChartConfig, getColorForDataKey, getDataKeys, @@ -92,8 +93,9 @@ const HorizontalBarChartComponent = ({ }: HorizontalBarChartProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; + const chartData = useMemo(() => ensureChartData(data), [data]); - const maxCategoryLabelWidth = useMaxCategoryLabelWidth(data, categoryKey as string); + const maxCategoryLabelWidth = useMaxCategoryLabelWidth(chartData, categoryKey as string); const chartContainerRef = useRef(null); const mainContainerRef = useRef(null); @@ -115,11 +117,15 @@ const HorizontalBarChartComponent = ({ // Calculate label height for better group height calculation // Use chart width for label height calculation since labels span full width - const labelHeight = useHorizontalBarLabelHeight(data, categoryKey as string, effectiveWidth); + const labelHeight = useHorizontalBarLabelHeight( + chartData, + categoryKey as string, + effectiveWidth, + ); const dataKeys = useMemo(() => { - return getDataKeys(data, categoryKey as string); - }, [data, categoryKey]); + return getDataKeys(chartData, categoryKey as string); + }, [chartData, categoryKey]); const transformedKeys = useTransformedKeys(dataKeys); @@ -146,17 +152,23 @@ const HorizontalBarChartComponent = ({ }, [effectiveHeight, showXAxis]); const padding = useMemo(() => { - return getPadding(data, categoryKey as string, effectiveContainerHeight, variant, labelHeight); - }, [data, categoryKey, effectiveContainerHeight, variant, labelHeight]); + return getPadding( + chartData, + categoryKey as string, + effectiveContainerHeight, + variant, + labelHeight, + ); + }, [chartData, categoryKey, effectiveContainerHeight, variant, labelHeight]); const dataHeight = useMemo(() => { - return getHeightOfData(data, categoryKey as string, variant, labelHeight); - }, [data, categoryKey, variant, labelHeight]); + return getHeightOfData(chartData, categoryKey as string, variant, labelHeight); + }, [chartData, categoryKey, variant, labelHeight]); // Calculate snap positions for proper group alignment const snapPositions = useMemo(() => { - return getSnapPositions(data, categoryKey as string, variant, labelHeight); - }, [data, categoryKey, variant, labelHeight]); + return getSnapPositions(chartData, categoryKey as string, variant, labelHeight); + }, [chartData, categoryKey, variant, labelHeight]); // Check scroll boundaries const updateScrollState = useCallback(() => { @@ -246,7 +258,7 @@ const HorizontalBarChartComponent = ({ const exportData = useExportChartData({ type: "bar", - data, + data: chartData, categoryKey: categoryKey as string, dataKeys, colors, @@ -276,7 +288,7 @@ const HorizontalBarChartComponent = ({ > ({ ); - }, [showXAxis, chartConfig, data, dataKeys, variant, id]); + }, [showXAxis, chartConfig, chartData, dataKeys, variant, id]); // Handle mouse events for group hovering const handleChartMouseMove = useCallback((state: any) => { @@ -379,7 +391,7 @@ const HorizontalBarChartComponent = ({ ({ }: LineChartCondensedProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; + const chartData = useMemo(() => ensureChartData(data), [data]); const dataKeys = useMemo(() => { - return getDataKeys(data, categoryKey as string); - }, [data, categoryKey]); + return getDataKeys(chartData, categoryKey as string); + }, [chartData, categoryKey]); const variant = getLineType(lineChartVariant); - const { yAxisWidth, setLabelWidth } = useYAxisLabelWidth(data, dataKeys); + const { yAxisWidth, setLabelWidth } = useYAxisLabelWidth(chartData, dataKeys); - const maxLabelWidth = useMaxLabelWidth(data, categoryKey as string); + const maxLabelWidth = useMaxLabelWidth(chartData, categoryKey as string); const chartContainerRef = useRef(null); const [chartContainerWidth, setChartContainerWidth] = useState(0); const widthOfData = useMemo(() => { - if (data.length === 0) { + if (chartData.length === 0) { return 0; } // Use passed width if available, otherwise use observed chartContainerWidth const chartWidth = width ?? chartContainerWidth; - return chartWidth / data.length; - }, [width, chartContainerWidth, data]); + return chartWidth / chartData.length; + }, [width, chartContainerWidth, chartData]); const { angle: calculatedAngle, height: xAxisHeight } = useAutoAngleCalculation( maxLabelWidth, @@ -140,7 +142,7 @@ const LineChartCondensedComponent = ({ const exportData = useExportChartData({ type: "line", - data, + data: chartData, categoryKey: categoryKey as string, dataKeys, colors, @@ -243,7 +245,7 @@ const LineChartCondensedComponent = ({ key={`y-axis-line-chart-condensed-${id}`} width={yAxisWidth} height={effectiveHeight} - data={data} + data={chartData} margin={{ top: chartMargin.top, bottom: xAxisHeight + chartMargin.bottom, // this is required to give space for x-axis @@ -276,7 +278,7 @@ const LineChartCondensedComponent = ({ }, [ showYAxis, effectiveHeight, - data, + chartData, dataKeys, id, yAxisWidth, @@ -318,7 +320,7 @@ const LineChartCondensedComponent = ({ diff --git a/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx b/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx index da669c8a6..c7e9ed15a 100644 --- a/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx +++ b/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx @@ -8,7 +8,7 @@ import { useExportChartData, useTransformedKeys } from "../hooks/index.js"; import { DefaultLegend } from "../shared/DefaultLegend/DefaultLegend.js"; import { StackedLegend } from "../shared/StackedLegend/StackedLegend.js"; import { LegendItem } from "../types/Legend.js"; -import { getCategoricalChartConfig } from "../utils/dataUtils.js"; +import { ensureChartData, getCategoricalChartConfig } from "../utils/dataUtils.js"; import { PaletteName, useChartPalette } from "../utils/PalletUtils.js"; import { PieChartData } from "./types/index.js"; import { @@ -76,6 +76,7 @@ const PieChartComponent = ({ }: PieChartProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; + const chartData = useMemo(() => ensureChartData(data), [data]); const wrapperRef = useRef(null); const [wrapperRect, setWrapperRect] = useState({ width: 0, height: 0 }); @@ -90,8 +91,8 @@ const PieChartComponent = ({ // Sort data by value (highest to lowest) for pie chart rendering const sortedProcessedData = useMemo( - () => [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), - [data, dataKey], + () => [...chartData].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), + [chartData, dataKey], ); const categories = useMemo( diff --git a/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx b/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx index 3eef96795..43f0f2914 100644 --- a/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx +++ b/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx @@ -14,7 +14,7 @@ import { useExportChartData, useTransformedKeys } from "../hooks"; import { ActiveDot, CustomTooltipContent, DefaultLegend } from "../shared"; import { LegendItem } from "../types"; import { useChartPalette } from "../utils/PalletUtils"; -import { get2dChartConfig, getDataKeys, getLegendItems } from "../utils/dataUtils"; +import { ensureChartData, get2dChartConfig, getDataKeys, getLegendItems } from "../utils/dataUtils"; import { AxisLabel } from "./components/AxisLabel"; import { RadarChartData } from "./types"; @@ -54,10 +54,11 @@ const RadarChartComponent = ({ }: RadarChartProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; + const chartData = useMemo(() => ensureChartData(data), [data]); const dataKeys = useMemo(() => { - return getDataKeys(data, categoryKey as string); - }, [data, categoryKey]); + return getDataKeys(chartData, categoryKey as string); + }, [chartData, categoryKey]); const transformedKeys = useTransformedKeys(dataKeys); @@ -79,7 +80,7 @@ const RadarChartComponent = ({ const exportData = useExportChartData({ type: "radar", - data, + data: chartData, categoryKey: categoryKey as string, dataKeys, colors, @@ -219,7 +220,7 @@ const RadarChartComponent = ({ rechartsProps={rechartsProps} > ({ }: RadialChartProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; + const chartData = useMemo(() => ensureChartData(data), [data]); const wrapperRef = useRef(null); const [wrapperRect, setWrapperRect] = useState({ width: 0, height: 0 }); @@ -83,8 +84,8 @@ export const RadialChart = ({ // Sort data by value (highest to lowest) for radial chart rendering const sortedProcessedData = useMemo( - () => [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), - [data, dataKey], + () => [...chartData].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), + [chartData, dataKey], ); const categories = useMemo( diff --git a/packages/react-ui/src/components/Charts/ScatterChart/ScatterChart.tsx b/packages/react-ui/src/components/Charts/ScatterChart/ScatterChart.tsx index eb4ad70ea..5bfcd0517 100644 --- a/packages/react-ui/src/components/Charts/ScatterChart/ScatterChart.tsx +++ b/packages/react-ui/src/components/Charts/ScatterChart/ScatterChart.tsx @@ -14,7 +14,7 @@ import { YAxisTick, } from "../shared"; import { LegendItem } from "../types"; -import { get2dChartConfig, getLegendItems } from "../utils/dataUtils"; +import { ensureChartData, get2dChartConfig, getLegendItems } from "../utils/dataUtils"; import { PaletteName, useChartPalette } from "../utils/PalletUtils"; import { numberTickFormatter } from "../utils/styleUtils"; import ScatterDot from "./components/ScatterDot"; @@ -63,10 +63,11 @@ export const ScatterChart = ({ }: ScatterChartProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; + const chartData = useMemo(() => ensureChartData(data), [data]); const datasets = useMemo(() => { - return getScatterDatasets(data); - }, [data]); + return getScatterDatasets(chartData); + }, [chartData]); const colors = useChartPalette({ chartThemeName: theme, @@ -76,11 +77,8 @@ export const ScatterChart = ({ }); const transformedData: ScatterPoint[] = useMemo(() => { - if (!data || !Array.isArray(data)) { - return []; - } - return transformScatterData(data, datasets, colors); - }, [data, datasets, colors]); + return transformScatterData(chartData, datasets, colors); + }, [chartData, datasets, colors]); const { yAxisWidth, setLabelWidth } = useYAxisLabelWidth(transformedData, [yAxisDataKey]); @@ -141,12 +139,12 @@ export const ScatterChart = ({ // Calculate domains for x and y axes const xDomain = useMemo(() => { - return calculateScatterDomain(data, xAxisDataKey as "x" | "y"); - }, [data, xAxisDataKey]); + return calculateScatterDomain(chartData, xAxisDataKey as "x" | "y"); + }, [chartData, xAxisDataKey]); const yDomain = useMemo(() => { - return calculateScatterDomain(data, yAxisDataKey as "x" | "y"); - }, [data, yAxisDataKey]); + return calculateScatterDomain(chartData, yAxisDataKey as "x" | "y"); + }, [chartData, yAxisDataKey]); const renderDotShape = useMemo(() => { return (props: unknown) => { @@ -187,13 +185,13 @@ export const ScatterChart = ({ const exportData = useExportChartData({ type: "scatter", - data, + data: chartData, colors, legend, xAxisLabel, yAxisLabel, customDataTransform: () => - data.map((dataset) => ({ + chartData.map((dataset) => ({ name: dataset.name, x: dataset.data.map((p) => p[xAxisDataKey] as number), y: dataset.data.map((p) => p[yAxisDataKey] as number), diff --git a/packages/react-ui/src/components/Charts/utils/dataUtils.ts b/packages/react-ui/src/components/Charts/utils/dataUtils.ts index fe0755e86..637fdf285 100644 --- a/packages/react-ui/src/components/Charts/utils/dataUtils.ts +++ b/packages/react-ui/src/components/Charts/utils/dataUtils.ts @@ -3,6 +3,10 @@ import { PieChartData } from "../PieChart"; import { RadialChartData } from "../RadialChart"; import { LegendItem } from "../types"; +export const ensureChartData = (data: T[] | null | undefined): T[] => { + return Array.isArray(data) ? data : []; +}; + /** * This function returns the data keys for the chart, used for the data keys of the chart. * @param data - The data to be displayed in the chart. From 67077e39d1cb05c04728336b9275d40d4ad175b7 Mon Sep 17 00:00:00 2001 From: CodexNexor Date: Mon, 11 May 2026 16:47:09 +0530 Subject: [PATCH 2/3] Fix formatting in chart null guard changes --- .../Charts/BarChartCondensed/BarChartCondensed.tsx | 9 ++++++++- .../Charts/HorizontalBarChart/HorizontalBarChart.tsx | 6 +----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx b/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx index abe7f41c7..5c3eb3f20 100644 --- a/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx +++ b/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx @@ -241,7 +241,14 @@ const BarChartCondensedComponent = ({ // Only apply maximum constraint, let Recharts handle thin bars automatically return Math.min(maxBarWidth, barWidth); - }, [explicitChartWidth, chartContainerWidth, chartData.length, dataKeys.length, variant, maxBarWidth]); + }, [ + explicitChartWidth, + chartContainerWidth, + chartData.length, + dataKeys.length, + variant, + maxBarWidth, + ]); // Handle mouse events for bar hovering const handleChartMouseMove = useCallback((state: any) => { diff --git a/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx b/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx index 520489198..b3c6b3223 100644 --- a/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx +++ b/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx @@ -117,11 +117,7 @@ const HorizontalBarChartComponent = ({ // Calculate label height for better group height calculation // Use chart width for label height calculation since labels span full width - const labelHeight = useHorizontalBarLabelHeight( - chartData, - categoryKey as string, - effectiveWidth, - ); + const labelHeight = useHorizontalBarLabelHeight(chartData, categoryKey as string, effectiveWidth); const dataKeys = useMemo(() => { return getDataKeys(chartData, categoryKey as string); From 556a592caf10c9ed05e9ec9ccd0a8544fdcd4ca1 Mon Sep 17 00:00:00 2001 From: CodexNexor Date: Mon, 11 May 2026 18:43:28 +0530 Subject: [PATCH 3/3] Move chart null guards into genui renderer --- .../AreaChartCondensed/AreaChartCondensed.tsx | 24 +- .../BarChartCondensed/BarChartCondensed.tsx | 37 +-- .../HorizontalBarChart/HorizontalBarChart.tsx | 36 +-- .../LineChartCondensed/LineChartCondensed.tsx | 24 +- .../components/Charts/PieChart/PieChart.tsx | 7 +- .../Charts/RadarChart/RadarChart.tsx | 11 +- .../Charts/RadialChart/RadialChart.tsx | 7 +- .../Charts/ScatterChart/ScatterChart.tsx | 26 +- .../src/components/Charts/utils/dataUtils.ts | 4 - .../react-ui/src/genui-lib/Charts/PieChart.ts | 15 +- .../src/genui-lib/Charts/RadialChart.ts | 13 +- .../src/genui-lib/Charts/ScatterChart.ts | 21 +- .../genui-lib/Charts/SingleStackedBarChart.ts | 13 +- packages/react-ui/src/genui-lib/helpers.ts | 260 ++++++++++++++++-- 14 files changed, 326 insertions(+), 172 deletions(-) diff --git a/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx b/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx index eb6f6faf0..ea3f13eda 100644 --- a/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx +++ b/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx @@ -27,7 +27,6 @@ import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip"; import { LegendItem } from "../types"; import { getLineType } from "../utils/AreaAndLine/common"; import { - ensureChartData, get2dChartConfig, getColorForDataKey, getDataKeys, @@ -82,29 +81,28 @@ const AreaChartCondensedComponent = ({ }: AreaChartCondensedProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; - const chartData = useMemo(() => ensureChartData(data), [data]); const dataKeys = useMemo(() => { - return getDataKeys(chartData, categoryKey as string); - }, [chartData, categoryKey]); + return getDataKeys(data, categoryKey as string); + }, [data, categoryKey]); const variant = getLineType(areaChartVariant); - const { yAxisWidth, setLabelWidth } = useYAxisLabelWidth(chartData, dataKeys); + const { yAxisWidth, setLabelWidth } = useYAxisLabelWidth(data, dataKeys); - const maxLabelWidth = useMaxLabelWidth(chartData, categoryKey as string); + const maxLabelWidth = useMaxLabelWidth(data, categoryKey as string); const chartContainerRef = useRef(null); const [chartContainerWidth, setChartContainerWidth] = useState(0); const widthOfData = useMemo(() => { - if (chartData.length === 0) { + if (data.length === 0) { return 0; } // Use passed width if available, otherwise use observed chartContainerWidth const chartWidth = width ?? chartContainerWidth; - return chartWidth / chartData.length; - }, [width, chartContainerWidth, chartData]); + return chartWidth / data.length; + }, [width, chartContainerWidth, data]); const { angle: calculatedAngle, height: xAxisHeight } = useAutoAngleCalculation( maxLabelWidth, @@ -141,7 +139,7 @@ const AreaChartCondensedComponent = ({ const exportData = useExportChartData({ type: "area", - data: chartData, + data, categoryKey: categoryKey as string, dataKeys, colors, @@ -242,7 +240,7 @@ const AreaChartCondensedComponent = ({ key={`y-axis-area-chart-condensed-${id}`} width={yAxisWidth} height={effectiveHeight} - data={chartData} + data={data} margin={{ top: chartMargin.top, bottom: xAxisHeight + chartMargin.bottom, // this is required to give space for x-axis @@ -275,7 +273,7 @@ const AreaChartCondensedComponent = ({ }, [ showYAxis, effectiveHeight, - chartData, + data, dataKeys, id, yAxisWidth, @@ -317,7 +315,7 @@ const AreaChartCondensedComponent = ({ diff --git a/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx b/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx index 5c3eb3f20..4f5089cba 100644 --- a/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx +++ b/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx @@ -28,7 +28,6 @@ import { LabelTooltipProvider } from "../shared/LabelTooltip/LabelTooltip"; import { LegendItem } from "../types"; import { getBarStackInfo, getRadiusArray } from "../utils/BarCharts/BarChartsUtils"; import { - ensureChartData, get2dChartConfig, getColorForDataKey, getDataKeys, @@ -96,27 +95,26 @@ const BarChartCondensedComponent = ({ }: BarChartCondensedProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; - const chartData = useMemo(() => ensureChartData(data), [data]); const dataKeys = useMemo(() => { - return getDataKeys(chartData, categoryKey as string); - }, [chartData, categoryKey]); + return getDataKeys(data, categoryKey as string); + }, [data, categoryKey]); - const { yAxisWidth, setLabelWidth } = useYAxisLabelWidth(chartData, dataKeys); + const { yAxisWidth, setLabelWidth } = useYAxisLabelWidth(data, dataKeys); - const maxLabelWidth = useMaxLabelWidth(chartData, categoryKey as string); + const maxLabelWidth = useMaxLabelWidth(data, categoryKey as string); const chartContainerRef = useRef(null); const [chartContainerWidth, setChartContainerWidth] = useState(0); const widthOfData = useMemo(() => { - if (chartData.length === 0) { + if (data.length === 0) { return 0; } // Use passed width if available, otherwise use observed chartContainerWidth const chartWidth = width ?? chartContainerWidth; - return chartWidth / chartData.length; - }, [chartContainerWidth, chartData, width]); + return chartWidth / data.length; + }, [chartContainerWidth, data, width]); const { angle: calculatedAngle, height: xAxisHeight } = useAutoAngleCalculation( maxLabelWidth, @@ -152,7 +150,7 @@ const BarChartCondensedComponent = ({ const exportData = useExportChartData({ type: "bar", - data: chartData, + data, categoryKey: categoryKey as string, dataKeys, colors, @@ -226,12 +224,12 @@ const BarChartCondensedComponent = ({ const availableWidth = explicitChartWidth ?? chartContainerWidth; // If no width available, return undefined and let Recharts auto-size - if (!availableWidth || availableWidth === 0 || chartData.length === 0) { + if (!availableWidth || availableWidth === 0 || data.length === 0) { return undefined; } // Calculate space per category (Recharts handles gaps automatically via barGap and barCategoryGap props) - const spacePerCategory = availableWidth / chartData.length; + const spacePerCategory = availableWidth / data.length; // For grouped charts, multiple bars share the category space const barsPerCategory = variant === "stacked" ? 1 : dataKeys.length; @@ -241,14 +239,7 @@ const BarChartCondensedComponent = ({ // Only apply maximum constraint, let Recharts handle thin bars automatically return Math.min(maxBarWidth, barWidth); - }, [ - explicitChartWidth, - chartContainerWidth, - chartData.length, - dataKeys.length, - variant, - maxBarWidth, - ]); + }, [explicitChartWidth, chartContainerWidth, data.length, dataKeys.length, variant, maxBarWidth]); // Handle mouse events for bar hovering const handleChartMouseMove = useCallback((state: any) => { @@ -334,7 +325,7 @@ const BarChartCondensedComponent = ({ key={`y-axis-bar-chart-condensed-${id}`} width={yAxisWidth} height={effectiveHeight} - data={chartData} + data={data} stackOffset="sign" margin={{ top: chartMargin.top, @@ -368,7 +359,7 @@ const BarChartCondensedComponent = ({ }, [ showYAxis, effectiveHeight, - chartData, + data, dataKeys, variant, id, @@ -473,7 +464,7 @@ const BarChartCondensedComponent = ({ stackOffset="sign" accessibilityLayer key={`bar-chart-condensed-${id}`} - data={chartData} + data={data} margin={chartMargin} barGap={BAR_GAP} barCategoryGap={BAR_CATEGORY_GAP} diff --git a/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx b/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx index b3c6b3223..568279e19 100644 --- a/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx +++ b/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx @@ -27,7 +27,6 @@ import { getRadiusArray, } from "../utils/BarCharts/BarChartsUtils"; import { - ensureChartData, get2dChartConfig, getColorForDataKey, getDataKeys, @@ -93,9 +92,8 @@ const HorizontalBarChartComponent = ({ }: HorizontalBarChartProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; - const chartData = useMemo(() => ensureChartData(data), [data]); - const maxCategoryLabelWidth = useMaxCategoryLabelWidth(chartData, categoryKey as string); + const maxCategoryLabelWidth = useMaxCategoryLabelWidth(data, categoryKey as string); const chartContainerRef = useRef(null); const mainContainerRef = useRef(null); @@ -117,11 +115,11 @@ const HorizontalBarChartComponent = ({ // Calculate label height for better group height calculation // Use chart width for label height calculation since labels span full width - const labelHeight = useHorizontalBarLabelHeight(chartData, categoryKey as string, effectiveWidth); + const labelHeight = useHorizontalBarLabelHeight(data, categoryKey as string, effectiveWidth); const dataKeys = useMemo(() => { - return getDataKeys(chartData, categoryKey as string); - }, [chartData, categoryKey]); + return getDataKeys(data, categoryKey as string); + }, [data, categoryKey]); const transformedKeys = useTransformedKeys(dataKeys); @@ -148,23 +146,17 @@ const HorizontalBarChartComponent = ({ }, [effectiveHeight, showXAxis]); const padding = useMemo(() => { - return getPadding( - chartData, - categoryKey as string, - effectiveContainerHeight, - variant, - labelHeight, - ); - }, [chartData, categoryKey, effectiveContainerHeight, variant, labelHeight]); + return getPadding(data, categoryKey as string, effectiveContainerHeight, variant, labelHeight); + }, [data, categoryKey, effectiveContainerHeight, variant, labelHeight]); const dataHeight = useMemo(() => { - return getHeightOfData(chartData, categoryKey as string, variant, labelHeight); - }, [chartData, categoryKey, variant, labelHeight]); + return getHeightOfData(data, categoryKey as string, variant, labelHeight); + }, [data, categoryKey, variant, labelHeight]); // Calculate snap positions for proper group alignment const snapPositions = useMemo(() => { - return getSnapPositions(chartData, categoryKey as string, variant, labelHeight); - }, [chartData, categoryKey, variant, labelHeight]); + return getSnapPositions(data, categoryKey as string, variant, labelHeight); + }, [data, categoryKey, variant, labelHeight]); // Check scroll boundaries const updateScrollState = useCallback(() => { @@ -254,7 +246,7 @@ const HorizontalBarChartComponent = ({ const exportData = useExportChartData({ type: "bar", - data: chartData, + data, categoryKey: categoryKey as string, dataKeys, colors, @@ -284,7 +276,7 @@ const HorizontalBarChartComponent = ({ > ({ ); - }, [showXAxis, chartConfig, chartData, dataKeys, variant, id]); + }, [showXAxis, chartConfig, data, dataKeys, variant, id]); // Handle mouse events for group hovering const handleChartMouseMove = useCallback((state: any) => { @@ -387,7 +379,7 @@ const HorizontalBarChartComponent = ({ ({ }: LineChartCondensedProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; - const chartData = useMemo(() => ensureChartData(data), [data]); const dataKeys = useMemo(() => { - return getDataKeys(chartData, categoryKey as string); - }, [chartData, categoryKey]); + return getDataKeys(data, categoryKey as string); + }, [data, categoryKey]); const variant = getLineType(lineChartVariant); - const { yAxisWidth, setLabelWidth } = useYAxisLabelWidth(chartData, dataKeys); + const { yAxisWidth, setLabelWidth } = useYAxisLabelWidth(data, dataKeys); - const maxLabelWidth = useMaxLabelWidth(chartData, categoryKey as string); + const maxLabelWidth = useMaxLabelWidth(data, categoryKey as string); const chartContainerRef = useRef(null); const [chartContainerWidth, setChartContainerWidth] = useState(0); const widthOfData = useMemo(() => { - if (chartData.length === 0) { + if (data.length === 0) { return 0; } // Use passed width if available, otherwise use observed chartContainerWidth const chartWidth = width ?? chartContainerWidth; - return chartWidth / chartData.length; - }, [width, chartContainerWidth, chartData]); + return chartWidth / data.length; + }, [width, chartContainerWidth, data]); const { angle: calculatedAngle, height: xAxisHeight } = useAutoAngleCalculation( maxLabelWidth, @@ -142,7 +140,7 @@ const LineChartCondensedComponent = ({ const exportData = useExportChartData({ type: "line", - data: chartData, + data, categoryKey: categoryKey as string, dataKeys, colors, @@ -245,7 +243,7 @@ const LineChartCondensedComponent = ({ key={`y-axis-line-chart-condensed-${id}`} width={yAxisWidth} height={effectiveHeight} - data={chartData} + data={data} margin={{ top: chartMargin.top, bottom: xAxisHeight + chartMargin.bottom, // this is required to give space for x-axis @@ -278,7 +276,7 @@ const LineChartCondensedComponent = ({ }, [ showYAxis, effectiveHeight, - chartData, + data, dataKeys, id, yAxisWidth, @@ -320,7 +318,7 @@ const LineChartCondensedComponent = ({ diff --git a/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx b/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx index c7e9ed15a..da669c8a6 100644 --- a/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx +++ b/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx @@ -8,7 +8,7 @@ import { useExportChartData, useTransformedKeys } from "../hooks/index.js"; import { DefaultLegend } from "../shared/DefaultLegend/DefaultLegend.js"; import { StackedLegend } from "../shared/StackedLegend/StackedLegend.js"; import { LegendItem } from "../types/Legend.js"; -import { ensureChartData, getCategoricalChartConfig } from "../utils/dataUtils.js"; +import { getCategoricalChartConfig } from "../utils/dataUtils.js"; import { PaletteName, useChartPalette } from "../utils/PalletUtils.js"; import { PieChartData } from "./types/index.js"; import { @@ -76,7 +76,6 @@ const PieChartComponent = ({ }: PieChartProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; - const chartData = useMemo(() => ensureChartData(data), [data]); const wrapperRef = useRef(null); const [wrapperRect, setWrapperRect] = useState({ width: 0, height: 0 }); @@ -91,8 +90,8 @@ const PieChartComponent = ({ // Sort data by value (highest to lowest) for pie chart rendering const sortedProcessedData = useMemo( - () => [...chartData].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), - [chartData, dataKey], + () => [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), + [data, dataKey], ); const categories = useMemo( diff --git a/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx b/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx index 43f0f2914..3eef96795 100644 --- a/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx +++ b/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx @@ -14,7 +14,7 @@ import { useExportChartData, useTransformedKeys } from "../hooks"; import { ActiveDot, CustomTooltipContent, DefaultLegend } from "../shared"; import { LegendItem } from "../types"; import { useChartPalette } from "../utils/PalletUtils"; -import { ensureChartData, get2dChartConfig, getDataKeys, getLegendItems } from "../utils/dataUtils"; +import { get2dChartConfig, getDataKeys, getLegendItems } from "../utils/dataUtils"; import { AxisLabel } from "./components/AxisLabel"; import { RadarChartData } from "./types"; @@ -54,11 +54,10 @@ const RadarChartComponent = ({ }: RadarChartProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; - const chartData = useMemo(() => ensureChartData(data), [data]); const dataKeys = useMemo(() => { - return getDataKeys(chartData, categoryKey as string); - }, [chartData, categoryKey]); + return getDataKeys(data, categoryKey as string); + }, [data, categoryKey]); const transformedKeys = useTransformedKeys(dataKeys); @@ -80,7 +79,7 @@ const RadarChartComponent = ({ const exportData = useExportChartData({ type: "radar", - data: chartData, + data, categoryKey: categoryKey as string, dataKeys, colors, @@ -220,7 +219,7 @@ const RadarChartComponent = ({ rechartsProps={rechartsProps} > ({ }: RadialChartProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; - const chartData = useMemo(() => ensureChartData(data), [data]); const wrapperRef = useRef(null); const [wrapperRect, setWrapperRect] = useState({ width: 0, height: 0 }); @@ -84,8 +83,8 @@ export const RadialChart = ({ // Sort data by value (highest to lowest) for radial chart rendering const sortedProcessedData = useMemo( - () => [...chartData].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), - [chartData, dataKey], + () => [...data].sort((a, b) => Number(b[dataKey]) - Number(a[dataKey])), + [data, dataKey], ); const categories = useMemo( diff --git a/packages/react-ui/src/components/Charts/ScatterChart/ScatterChart.tsx b/packages/react-ui/src/components/Charts/ScatterChart/ScatterChart.tsx index 5bfcd0517..eb4ad70ea 100644 --- a/packages/react-ui/src/components/Charts/ScatterChart/ScatterChart.tsx +++ b/packages/react-ui/src/components/Charts/ScatterChart/ScatterChart.tsx @@ -14,7 +14,7 @@ import { YAxisTick, } from "../shared"; import { LegendItem } from "../types"; -import { ensureChartData, get2dChartConfig, getLegendItems } from "../utils/dataUtils"; +import { get2dChartConfig, getLegendItems } from "../utils/dataUtils"; import { PaletteName, useChartPalette } from "../utils/PalletUtils"; import { numberTickFormatter } from "../utils/styleUtils"; import ScatterDot from "./components/ScatterDot"; @@ -63,11 +63,10 @@ export const ScatterChart = ({ }: ScatterChartProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; - const chartData = useMemo(() => ensureChartData(data), [data]); const datasets = useMemo(() => { - return getScatterDatasets(chartData); - }, [chartData]); + return getScatterDatasets(data); + }, [data]); const colors = useChartPalette({ chartThemeName: theme, @@ -77,8 +76,11 @@ export const ScatterChart = ({ }); const transformedData: ScatterPoint[] = useMemo(() => { - return transformScatterData(chartData, datasets, colors); - }, [chartData, datasets, colors]); + if (!data || !Array.isArray(data)) { + return []; + } + return transformScatterData(data, datasets, colors); + }, [data, datasets, colors]); const { yAxisWidth, setLabelWidth } = useYAxisLabelWidth(transformedData, [yAxisDataKey]); @@ -139,12 +141,12 @@ export const ScatterChart = ({ // Calculate domains for x and y axes const xDomain = useMemo(() => { - return calculateScatterDomain(chartData, xAxisDataKey as "x" | "y"); - }, [chartData, xAxisDataKey]); + return calculateScatterDomain(data, xAxisDataKey as "x" | "y"); + }, [data, xAxisDataKey]); const yDomain = useMemo(() => { - return calculateScatterDomain(chartData, yAxisDataKey as "x" | "y"); - }, [chartData, yAxisDataKey]); + return calculateScatterDomain(data, yAxisDataKey as "x" | "y"); + }, [data, yAxisDataKey]); const renderDotShape = useMemo(() => { return (props: unknown) => { @@ -185,13 +187,13 @@ export const ScatterChart = ({ const exportData = useExportChartData({ type: "scatter", - data: chartData, + data, colors, legend, xAxisLabel, yAxisLabel, customDataTransform: () => - chartData.map((dataset) => ({ + data.map((dataset) => ({ name: dataset.name, x: dataset.data.map((p) => p[xAxisDataKey] as number), y: dataset.data.map((p) => p[yAxisDataKey] as number), diff --git a/packages/react-ui/src/components/Charts/utils/dataUtils.ts b/packages/react-ui/src/components/Charts/utils/dataUtils.ts index 637fdf285..fe0755e86 100644 --- a/packages/react-ui/src/components/Charts/utils/dataUtils.ts +++ b/packages/react-ui/src/components/Charts/utils/dataUtils.ts @@ -3,10 +3,6 @@ import { PieChartData } from "../PieChart"; import { RadialChartData } from "../RadialChart"; import { LegendItem } from "../types"; -export const ensureChartData = (data: T[] | null | undefined): T[] => { - return Array.isArray(data) ? data : []; -}; - /** * This function returns the data keys for the chart, used for the data keys of the chart. * @param data - The data to be displayed in the chart. diff --git a/packages/react-ui/src/genui-lib/Charts/PieChart.ts b/packages/react-ui/src/genui-lib/Charts/PieChart.ts index f513554b1..b58696296 100644 --- a/packages/react-ui/src/genui-lib/Charts/PieChart.ts +++ b/packages/react-ui/src/genui-lib/Charts/PieChart.ts @@ -4,7 +4,7 @@ import { defineComponent } from "@openuidev/react-lang"; import React from "react"; import { z } from "zod/v4"; import { PieChart as PieChartComponent } from "../../components/Charts"; -import { asArray, buildSliceData } from "../helpers"; +import { buildLabeledValueData, buildSliceData } from "../helpers"; export const PieChartSchema = z.object({ labels: z.array(z.string()), @@ -17,16 +17,8 @@ export const PieChart = defineComponent({ props: PieChartSchema, description: "Circular slices; use plucked arrays: PieChart(data.categories, data.values)", component: ({ props }) => { - const labels = asArray(props.labels) as string[]; - const values = asArray(props.values) as number[]; - - // New format: labels[] + values[] - if (labels.length > 0 && values.length > 0) { - const data = labels.map((cat, i) => ({ - category: cat, - value: typeof values[i] === "number" ? values[i] : 0, - })); - if (!data.length) return null; + const data = buildLabeledValueData(props.labels, props.values); + if (data.length > 0) { return React.createElement(PieChartComponent, { data, categoryKey: "category", @@ -36,7 +28,6 @@ export const PieChart = defineComponent({ }); } - // Legacy fallback: Slice[] objects (backwards compat) const sliceData = buildSliceData(props.labels); if (sliceData.length) { return React.createElement(PieChartComponent, { diff --git a/packages/react-ui/src/genui-lib/Charts/RadialChart.ts b/packages/react-ui/src/genui-lib/Charts/RadialChart.ts index bb94daa31..45ecfd53f 100644 --- a/packages/react-ui/src/genui-lib/Charts/RadialChart.ts +++ b/packages/react-ui/src/genui-lib/Charts/RadialChart.ts @@ -4,7 +4,7 @@ import { defineComponent } from "@openuidev/react-lang"; import React from "react"; import { z } from "zod/v4"; import { RadialChart as RadialChartComponent } from "../../components/Charts"; -import { asArray, buildSliceData } from "../helpers"; +import { buildLabeledValueData, buildSliceData } from "../helpers"; export const RadialChartSchema = z.object({ labels: z.array(z.string()), @@ -16,15 +16,8 @@ export const RadialChart = defineComponent({ props: RadialChartSchema, description: "Radial bars; use plucked arrays: RadialChart(data.categories, data.values)", component: ({ props }) => { - const labels = asArray(props.labels) as string[]; - const values = asArray(props.values) as number[]; - - if (labels.length > 0 && values.length > 0) { - const data = labels.map((cat, i) => ({ - category: cat, - value: typeof values[i] === "number" ? values[i] : 0, - })); - if (!data.length) return null; + const data = buildLabeledValueData(props.labels, props.values); + if (data.length > 0) { return React.createElement(RadialChartComponent, { data, categoryKey: "category", diff --git a/packages/react-ui/src/genui-lib/Charts/ScatterChart.ts b/packages/react-ui/src/genui-lib/Charts/ScatterChart.ts index ba00a2fe1..fd5f091f8 100644 --- a/packages/react-ui/src/genui-lib/Charts/ScatterChart.ts +++ b/packages/react-ui/src/genui-lib/Charts/ScatterChart.ts @@ -4,7 +4,7 @@ import { defineComponent } from "@openuidev/react-lang"; import React from "react"; import { z } from "zod/v4"; import { ScatterChart as ScatterChartComponent } from "../../components/Charts"; -import { asArray, hasAllProps } from "../helpers"; +import { buildScatterChartData, hasAllProps } from "../helpers"; import { ScatterSeriesSchema } from "./ScatterSeries"; export const ScatterChartSchema = z.object({ @@ -13,30 +13,13 @@ export const ScatterChartSchema = z.object({ yLabel: z.string().optional(), }); -const unwrap = (node: any) => (node?.type === "element" ? node.props : node); - export const ScatterChart = defineComponent({ name: "ScatterChart", props: ScatterChartSchema, description: "X/Y scatter plot; use for correlations, distributions, and clustering", component: ({ props }) => { if (!hasAllProps(props as Record, "datasets")) return null; - const rawDatasets = asArray((props as any).datasets); - const data = rawDatasets.map((ds: any) => { - const dsProps = unwrap(ds); - const rawPoints = asArray(dsProps?.points); - return { - name: (dsProps?.name ?? "") as string, - data: rawPoints.map((pt: any) => { - const ptProps = unwrap(pt); - return { - x: Number(ptProps?.x), - y: Number(ptProps?.y), - ...(ptProps?.z != null ? { z: Number(ptProps.z) } : {}), - }; - }), - }; - }); + const data = buildScatterChartData((props as Record).datasets); if (!data.length) return null; return React.createElement(ScatterChartComponent, { data, diff --git a/packages/react-ui/src/genui-lib/Charts/SingleStackedBarChart.ts b/packages/react-ui/src/genui-lib/Charts/SingleStackedBarChart.ts index f3584a13a..4db55600d 100644 --- a/packages/react-ui/src/genui-lib/Charts/SingleStackedBarChart.ts +++ b/packages/react-ui/src/genui-lib/Charts/SingleStackedBarChart.ts @@ -4,7 +4,7 @@ import { defineComponent } from "@openuidev/react-lang"; import React from "react"; import { z } from "zod/v4"; import { SingleStackedBar as SingleStackedBarChartComponent } from "../../components/Charts"; -import { asArray, buildSliceData } from "../helpers"; +import { buildLabeledValueData, buildSliceData } from "../helpers"; export const SingleStackedBarChartSchema = z.object({ labels: z.array(z.string()), @@ -17,15 +17,8 @@ export const SingleStackedBarChart = defineComponent({ description: "Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)", component: ({ props }) => { - const labels = asArray(props.labels) as string[]; - const values = asArray(props.values) as number[]; - - if (labels.length > 0 && values.length > 0) { - const data = labels.map((cat, i) => ({ - category: cat, - value: typeof values[i] === "number" ? values[i] : 0, - })); - if (!data.length) return null; + const data = buildLabeledValueData(props.labels, props.values); + if (data.length > 0) { return React.createElement(SingleStackedBarChartComponent, { data, categoryKey: "category", diff --git a/packages/react-ui/src/genui-lib/helpers.ts b/packages/react-ui/src/genui-lib/helpers.ts index 767cbf0a4..31d121423 100644 --- a/packages/react-ui/src/genui-lib/helpers.ts +++ b/packages/react-ui/src/genui-lib/helpers.ts @@ -3,6 +3,33 @@ type ElementLike = { props: Record; }; +type ScatterPointLike = { + x: number; + y: number; + z?: number; +}; + +type ScatterDatasetLike = { + name: string; + data: ScatterPointLike[]; +}; + +type ChartPoint = Record; + +type NamedSeries = { + category: string; + values: number[]; +}; + +type SliceDatum = { + category: string; + value: number; +}; + +function isDefined(value: T | null): value is T { + return value !== null; +} + export function hasAllProps(obj: Record, ...keys: string[]): boolean { return keys.every((k) => obj[k] != null); } @@ -20,11 +47,70 @@ function asElementNodes(v: unknown): ElementLike[] { ); } -export function buildChartData( - labels: unknown, - series: unknown, -): Record[] { - const lbls = asArray(labels) as string[]; +function unwrapProps(v: unknown): Record { + if (typeof v !== "object" || v === null) { + return {}; + } + + if ((v as Record)["type"] === "element") { + const props = (v as Record)["props"]; + if (typeof props === "object" && props !== null) { + return props as Record; + } + } + + return v as Record; +} + +function asStringArray(v: unknown): string[] | null { + const values = asArray(v); + if (values.length === 0) { + return []; + } + + if (!values.every((value) => typeof value === "string")) { + return null; + } + + return values; +} + +function asFiniteNumber(v: unknown): number | null { + if (typeof v === "number") { + return Number.isFinite(v) ? v : null; + } + + if (typeof v === "string" && v.trim() !== "") { + const parsed = Number(v); + return Number.isFinite(parsed) ? parsed : null; + } + + return null; +} + +function asNumberArray(v: unknown): number[] | null { + const values = asArray(v); + if (values.length === 0) { + return []; + } + + const normalized: number[] = []; + for (const value of values) { + const parsed = asFiniteNumber(value); + if (parsed === null) { + return null; + } + normalized.push(parsed); + } + + return normalized; +} + +export function buildChartData(labels: unknown, series: unknown): ChartPoint[] { + const lbls = asStringArray(labels); + if (lbls === null || lbls.length === 0) { + return []; + } // Tabular format: labels = column names, series = 2D rows from Query results // e.g. AreaChart(data.columns, data.results) where columns=["day","views","users"] @@ -33,35 +119,169 @@ export function buildChartData( if (rows.length > 0 && Array.isArray(rows[0])) { // Column 0 = category labels, columns 1+ = series values const seriesNames = lbls.slice(1); - return rows.map((row) => { + if (seriesNames.length === 0) { + return []; + } + + const tabularData = rows.map((row): ChartPoint | null => { + if (!Array.isArray(row)) { + return null; + } + const cells = row as unknown[]; - const point: Record = { category: String(cells[0] ?? "") }; + if (cells.length < lbls.length || cells[0] == null) { + return null; + } + + const point: ChartPoint = { category: String(cells[0]) }; seriesNames.forEach((name, si) => { - const val = cells[si + 1]; - point[name] = typeof val === "number" ? val : Number(val) || 0; + const val = asFiniteNumber(cells[si + 1]); + if (val === null) { + point[name] = Number.NaN; + return; + } + point[name] = val; }); + + if (seriesNames.some((name) => !Number.isFinite(point[name] as number))) { + return null; + } + return point; }); + + if (tabularData.some((point) => point === null)) { + return []; + } + + return tabularData.filter(isDefined); } // Original format: labels = x-axis values, series = Series() elements const seriesNodes = asElementNodes(series); + if (seriesNodes.length === 0) { + return []; + } + + const normalizedSeries = seriesNodes.map((node): NamedSeries | null => { + const category = node.props["category"]; + const values = asNumberArray(node.props["values"]); + + if (typeof category !== "string" || values === null || values.length !== lbls.length) { + return null; + } + + return { category, values }; + }); + + if (normalizedSeries.some((entry) => entry === null)) { + return []; + } + + const seriesData = normalizedSeries.filter(isDefined); + return lbls.map((label, i) => { - const point: Record = { category: label }; - seriesNodes.forEach((s) => { - const cat = s.props["category"]; - const vals = s.props["values"]; - if (typeof cat === "string" && Array.isArray(vals) && i < vals.length) { - point[cat] = vals[i]!; - } + const point: ChartPoint = { category: label }; + seriesData.forEach((entry) => { + point[entry.category] = entry.values[i]!; }); return point; }); } -export function buildSliceData(slices: unknown): Record[] { - return asElementNodes(slices).map((s) => ({ - category: s.props["category"] as string, - value: s.props["value"] as number, +export function buildLabeledValueData(labels: unknown, values: unknown): ChartPoint[] { + const categories = asStringArray(labels); + const normalizedValues = asNumberArray(values); + + if ( + categories === null || + normalizedValues === null || + categories.length === 0 || + categories.length !== normalizedValues.length + ) { + return []; + } + + return categories.map((category, index) => ({ + category, + value: normalizedValues[index]!, })); } + +export function buildSliceData(slices: unknown): Record[] { + const sliceNodes = asElementNodes(slices); + if (sliceNodes.length === 0) { + return []; + } + + const normalizedSlices = sliceNodes.map((slice): SliceDatum | null => { + const category = slice.props["category"]; + const value = asFiniteNumber(slice.props["value"]); + + if (typeof category !== "string" || value === null) { + return null; + } + + return { category, value }; + }); + + if (normalizedSlices.some((slice) => slice === null)) { + return []; + } + + return normalizedSlices.filter(isDefined); +} + +export function buildScatterChartData(datasets: unknown): ScatterDatasetLike[] { + const rawDatasets = asArray(datasets); + if (rawDatasets.length === 0) { + return []; + } + + const normalizedDatasets = rawDatasets.map((dataset): ScatterDatasetLike | null => { + const datasetProps = unwrapProps(dataset); + const name = datasetProps["name"]; + const rawPoints = asArray(datasetProps["points"]); + + if (typeof name !== "string" || rawPoints.length === 0) { + return null; + } + + const points = rawPoints.map((point) => { + const pointProps = unwrapProps(point); + const x = asFiniteNumber(pointProps["x"]); + const y = asFiniteNumber(pointProps["y"]); + const rawZ = pointProps["z"]; + + if (x === null || y === null) { + return null; + } + + if (rawZ == null) { + return { x, y }; + } + + const z = asFiniteNumber(rawZ); + if (z === null) { + return null; + } + + return { x, y, z }; + }); + + if (points.some((point) => point === null)) { + return null; + } + + return { + name, + data: points as ScatterPointLike[], + }; + }); + + if (normalizedDatasets.some((dataset) => dataset === null)) { + return []; + } + + return normalizedDatasets.filter(isDefined); +}