diff --git a/js/packages/react-ui/package.json b/js/packages/react-ui/package.json index 2d45b7be..dfd4ab3d 100644 --- a/js/packages/react-ui/package.json +++ b/js/packages/react-ui/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@crayonai/react-ui", "license": "MIT", - "version": "0.9.15", + "version": "0.9.16", "description": "Component library for Generative UI SDK", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/js/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx b/js/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx index 54b3b253..d412acf5 100644 --- a/js/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx +++ b/js/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx @@ -100,8 +100,10 @@ const AreaChartCondensedComponent = ({ if (data.length === 0) { return 0; } - return chartContainerWidth / data.length; - }, [chartContainerWidth, data]); + // Use passed width if available, otherwise use observed chartContainerWidth + const chartWidth = width ?? chartContainerWidth; + return chartWidth / data.length; + }, [width, chartContainerWidth, data]); const { angle: calculatedAngle, height: xAxisHeight } = useAutoAngleCalculation( maxLabelWidth, diff --git a/js/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx b/js/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx index 8ece2598..f2d61b5d 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx @@ -59,9 +59,14 @@ export interface BarChartCondensedProps { className?: string; height?: number; width?: number; + /** Maximum bar width in pixels. Prevents bars from becoming too wide. Default: 12 */ + maxBarWidth?: number; } -const BAR_WIDTH = 12; +// Default maximum bar width - prevents bars from becoming too wide with sparse data +const DEFAULT_MAX_BAR_WIDTH = 12; + +// Layout constants const BAR_GAP = 10; const BAR_CATEGORY_GAP = "20%"; const BAR_INTERNAL_LINE_WIDTH = 1; @@ -87,6 +92,7 @@ const BarChartCondensedComponent = ({ className, height = CHART_HEIGHT, width, + maxBarWidth = DEFAULT_MAX_BAR_WIDTH, }: BarChartCondensedProps) => { const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; @@ -106,8 +112,10 @@ const BarChartCondensedComponent = ({ if (data.length === 0) { return 0; } - return chartContainerWidth / data.length; - }, [chartContainerWidth, data]); + // Use passed width if available, otherwise use observed chartContainerWidth + const chartWidth = width ?? chartContainerWidth; + return chartWidth / data.length; + }, [chartContainerWidth, data, width]); const { angle: calculatedAngle, height: xAxisHeight } = useAutoAngleCalculation( maxLabelWidth, @@ -201,6 +209,39 @@ const BarChartCondensedComponent = ({ return width ?? containerWidth; }, [width, containerWidth]); + // Calculate explicit chart width when width prop is provided + // This allows Recharts to calculate bar dimensions on first render + const explicitChartWidth = useMemo(() => { + if (!width) return undefined; + // Subtract Y-axis width and margins to get the actual chart area width + const yAxisSpace = showYAxis ? yAxisWidth : 0; + return width - yAxisSpace - chartMargin.left - chartMargin.right; + }, [width, showYAxis, yAxisWidth, chartMargin.left, chartMargin.right]); + + // Calculate optimal bar width based on available space + // Only applies maximum constraint - Recharts handles thin bars automatically + const calculatedBarWidth = useMemo(() => { + // Use explicitChartWidth if available, otherwise fall back to chartContainerWidth + const availableWidth = explicitChartWidth ?? chartContainerWidth; + + // If no width available, return undefined and let Recharts auto-size + 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 / data.length; + + // For grouped charts, multiple bars share the category space + const barsPerCategory = variant === "stacked" ? 1 : dataKeys.length; + + // Simple division - let Recharts apply gaps via barGap and barCategoryGap props + const barWidth = spacePerCategory / barsPerCategory; + + // Only apply maximum constraint, let Recharts handle thin bars automatically + return Math.min(maxBarWidth, barWidth); + }, [explicitChartWidth, chartContainerWidth, data.length, dataKeys.length, variant, maxBarWidth]); + // Handle mouse events for bar hovering const handleChartMouseMove = useCallback((state: any) => { if (state && state.activeLabel !== undefined) { @@ -231,15 +272,16 @@ const BarChartCondensedComponent = ({ // Observe container width for legend useEffect(() => { - // Only set up ResizeObserver if width is not provided - if (width || !containerRef.current || !chartContainerRef.current) { + // Always set up ResizeObserver for chartContainerRef to get accurate bar width calculations + if (!chartContainerRef.current) { return () => {}; } const resizeObserver = new ResizeObserver((entries) => { // there is only one entry in the entries array because we are observing the chart container for (const entry of entries) { - if (entry.target === containerRef.current) { + if (entry.target === containerRef.current && !width) { + // Only observe containerRef if width is not provided setContainerWidth(entry.contentRect.width); } if (entry.target === chartContainerRef.current) { @@ -248,9 +290,14 @@ const BarChartCondensedComponent = ({ } }); - resizeObserver.observe(containerRef.current); + // Always observe chartContainerRef resizeObserver.observe(chartContainerRef.current); + // Only observe containerRef if width is not provided + if (!width && containerRef.current) { + resizeObserver.observe(containerRef.current); + } + return () => { resizeObserver.disconnect(); }; @@ -335,8 +382,8 @@ const BarChartCondensedComponent = ({ fill={color} stackId={variant === "stacked" ? "a" : undefined} isAnimationActive={isAnimationActive} - maxBarSize={BAR_WIDTH} - barSize={BAR_WIDTH} + maxBarSize={calculatedBarWidth} + barSize={calculatedBarWidth} shape={(props: any) => { const { payload, value, dataKey } = props; @@ -378,6 +425,7 @@ const BarChartCondensedComponent = ({ barInternalLineColor, hoveredCategory, categoryKey, + calculatedBarWidth, ]); return ( @@ -404,10 +452,13 @@ const BarChartCondensedComponent = ({
({ onMouseMove={handleChartMouseMove} onMouseLeave={handleChartMouseLeave} onClick={onBarClick} + width={explicitChartWidth} + height={effectiveHeight} > {grid && cartesianGrid()} diff --git a/js/packages/react-ui/src/components/Charts/BarChartCondensed/stories/barChartCondensed.stories.tsx b/js/packages/react-ui/src/components/Charts/BarChartCondensed/stories/barChartCondensed.stories.tsx index f97ce8d7..3116cc7c 100644 --- a/js/packages/react-ui/src/components/Charts/BarChartCondensed/stories/barChartCondensed.stories.tsx +++ b/js/packages/react-ui/src/components/Charts/BarChartCondensed/stories/barChartCondensed.stories.tsx @@ -265,7 +265,7 @@ const salesData = [ }, }, }, - tags: ["!dev", "autodocs"], + tags: ["dev", "autodocs"], argTypes: { data: { description: ` @@ -1409,3 +1409,366 @@ export const ResponsiveBehaviorDemo: Story = { ); }, }; + +/** + * 🎯 Bar Width Testing Playground + * Interactive story to test bar width calculations with various configurations: + * - Different data set sizes (few bars vs many bars) + * - Grouped vs Stacked variants + * - Max bar width constraint (prevents bars from being too wide) + * - Container width variations + */ +export const BarWidthPlayground: StoryObj = { + render: () => { + const [variant, setVariant] = useState<"grouped" | "stacked">("grouped"); + const [dataSize, setDataSize] = useState<"small" | "medium" | "large" | "xlarge">("medium"); + const [maxBarWidth, setMaxBarWidth] = useState(60); + const [containerWidth, setContainerWidth] = useState(undefined); + + // Generate data based on size selection + const generateData = () => { + const sizes = { + small: 4, + medium: 12, + large: 24, + xlarge: 50, + }; + + const count = sizes[dataSize]; + return Array.from({ length: count }, (_, i) => ({ + quarter: `Q${(i % 4) + 1} FY${2022 + Math.floor(i / 4)}`, + revenue: Math.floor(Math.random() * 2000000) + 500000, + expenses: Math.floor(Math.random() * 1500000) + 300000, + profit: Math.floor(Math.random() * 800000) + 100000, + })); + }; + + const data = generateData(); + + return ( +
+ {/* Control Panel */} + +

+ 🎛️ Bar Width Controls +

+ +
+ {/* Variant Control */} +
+ +
+ + +
+
+ + {/* Data Size Control */} +
+ + +
+ + {/* Max Bar Width Control */} +
+ + setMaxBarWidth(Number(e.target.value))} + style={{ width: "100%" }} + /> +
+ 10px + 120px +
+
+ + {/* Container Width Control */} +
+ +
+ setContainerWidth(Number(e.target.value))} + style={{ flex: 1 }} + disabled={!containerWidth} + /> + +
+
+ 400px + 1400px +
+
+
+ + {/* Info Display */} +
+
+
+ Data Points: {data.length} +
+
+ Bars per Category: {variant === "stacked" ? 1 : 3} +
+
+ Total Bars: {data.length * (variant === "stacked" ? 1 : 3)} +
+
+
+
+ + {/* Chart Display */} + +

+ {variant === "grouped" ? "📊 Grouped" : "📚 Stacked"} Bar Chart - Bar Width Test +

+
+ +
+
+ + {/* Quick Test Presets */} + +

+ 🚀 Quick Test Presets +

+
+ + + + + + + + + + + +
+
+
+ ); + }, +}; diff --git a/js/packages/react-ui/src/components/Charts/LineChartCondensed/LineChartCondensed.tsx b/js/packages/react-ui/src/components/Charts/LineChartCondensed/LineChartCondensed.tsx index 8ee1fe9e..aabf781b 100644 --- a/js/packages/react-ui/src/components/Charts/LineChartCondensed/LineChartCondensed.tsx +++ b/js/packages/react-ui/src/components/Charts/LineChartCondensed/LineChartCondensed.tsx @@ -102,8 +102,10 @@ const LineChartCondensedComponent = ({ if (data.length === 0) { return 0; } - return chartContainerWidth / data.length; - }, [chartContainerWidth, data]); + // Use passed width if available, otherwise use observed chartContainerWidth + const chartWidth = width ?? chartContainerWidth; + return chartWidth / data.length; + }, [width, chartContainerWidth, data]); const { angle: calculatedAngle, height: xAxisHeight } = useAutoAngleCalculation( maxLabelWidth, diff --git a/js/packages/react-ui/src/components/Charts/shared/LineInBarShape/LineInBarShape.tsx b/js/packages/react-ui/src/components/Charts/shared/LineInBarShape/LineInBarShape.tsx index f984adef..a55b9e94 100644 --- a/js/packages/react-ui/src/components/Charts/shared/LineInBarShape/LineInBarShape.tsx +++ b/js/packages/react-ui/src/components/Charts/shared/LineInBarShape/LineInBarShape.tsx @@ -23,7 +23,8 @@ interface LineInBarShapeProps { } const DEFAULT_STACK_GAP = 1; -const MIN_LINE_DIMENSION = 8; // For internal line visibility +const MIN_LINE_DIMENSION = 8; // For internal line visibility (height/width threshold) +const MIN_BAR_WIDTH_FOR_LINE = 3; // Minimum bar width to show internal line in vertical mode const LINE_PADDING = 6; const MIN_GROUP_BAR_HEIGHT = 2; // For vertical bars const MIN_STACKED_BAR_HEIGHT = 4; // For vertical bars @@ -302,7 +303,11 @@ const LineInBarShape: FunctionComponent = React.memo((props const lineCoords = useMemo(() => { if (isVertical) { - if (width <= 0 || adjustedHeight < MIN_LINE_DIMENSION) return null; + // For vertical bars: hide line if bar width < 3px (too thin) OR height < 8px (too short) + // This ensures thin bars don't have a visible internal line that looks awkward + if (width <= 0 || width < MIN_BAR_WIDTH_FOR_LINE || adjustedHeight < MIN_LINE_DIMENSION) { + return null; + } const centerX = x + width / 2; return { x1: centerX, @@ -311,7 +316,7 @@ const LineInBarShape: FunctionComponent = React.memo((props y2: adjustedY + adjustedHeight - LINE_PADDING, }; } - // Horizontal + // Horizontal bars: hide line if width < 8px OR height <= 0 if (adjustedWidth < MIN_LINE_DIMENSION || height <= 0) return null; const centerY = y + height / 2; return {